Add JWT-based security and /me endpoint
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Introduced JWT-based authentication with role handling using Keycloak. Added the `/me` endpoint to return user information and roles. Configured testing, Keycloak integration, and public-facing `/public/info` endpoint enhancements.
This commit is contained in:
@ -0,0 +1,42 @@
|
||||
package se.urmo.hemhub.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class KeycloakRealmRoleConverterTest {
|
||||
|
||||
@Test
|
||||
void picksRealmRoles_scopes_andClientRoles() {
|
||||
var jwt = new Jwt(
|
||||
"t", Instant.now(), Instant.now().plusSeconds(3600),
|
||||
Map.of("alg","none"),
|
||||
Map.of(
|
||||
"realm_access", Map.of("roles", List.of("OWNER","MEMBER")),
|
||||
"resource_access", Map.of(
|
||||
"hemhub-public", Map.of("roles", List.of("reader"))
|
||||
),
|
||||
"scope", "read write"
|
||||
)
|
||||
);
|
||||
|
||||
var conv = new KeycloakRealmRoleConverter();
|
||||
var auths = conv.convert(jwt);
|
||||
|
||||
assertThat(auths).extracting("authority")
|
||||
.contains("ROLE_OWNER","ROLE_MEMBER","ROLE_reader","ROLE_read","ROLE_write");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tolerantToMissingFields_returnsEmpty() {
|
||||
var jwt = new Jwt("t", Instant.now(), Instant.now().plusSeconds(3600),
|
||||
Map.of("alg","none"), Map.of("dummy", "value"));
|
||||
var conv = new KeycloakRealmRoleConverter();
|
||||
assertThat(conv.convert(jwt)).isEmpty();
|
||||
}
|
||||
}
|
||||
32
src/test/java/se/urmo/hemhub/security/JwtTestConfig.java
Normal file
32
src/test/java/se/urmo/hemhub/security/JwtTestConfig.java
Normal file
@ -0,0 +1,32 @@
|
||||
package se.urmo.hemhub.security;
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.oauth2.jwt.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@TestConfiguration
|
||||
public class JwtTestConfig {
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
return token -> {
|
||||
// tokenformat i test: "HID:ROLE1,ROLE2"
|
||||
String[] parts = token.split(":");
|
||||
String hid = parts.length > 0 ? parts[0] : "H-TEST";
|
||||
List<String> roles = List.of("MEMBER");
|
||||
Instant now = Instant.now();
|
||||
|
||||
return new Jwt(token, now, now.plusSeconds(3600),
|
||||
Map.of("alg","none"),
|
||||
Map.of(
|
||||
"sub","test-user",
|
||||
"email","test@example.com",
|
||||
"preferred_username","test",
|
||||
"household_id", hid,
|
||||
"realm_access", Map.of("roles", roles)
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
47
src/test/java/se/urmo/hemhub/web/MeControllerBranchesIT.java
Normal file
47
src/test/java/se/urmo/hemhub/web/MeControllerBranchesIT.java
Normal file
@ -0,0 +1,47 @@
|
||||
package se.urmo.hemhub.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class MeControllerBranchesIT {
|
||||
|
||||
@Autowired MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void me_withoutRealmAccess_rolesEmpty() throws Exception {
|
||||
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||
j.subject("u1");
|
||||
j.claim("preferred_username", "test1");
|
||||
j.claim("household_id", "H1");
|
||||
// ingen realm_access-claim
|
||||
})))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.roles").isArray())
|
||||
.andExpect(jsonPath("$.roles.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void me_withMalformedRealmAccess_rolesEmpty() throws Exception {
|
||||
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||
j.subject("u2");
|
||||
j.claim("preferred_username", "test2");
|
||||
j.claim("household_id", "H2");
|
||||
j.claim("realm_access", Map.of("roles", "NOT_A_LIST"));
|
||||
})))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.roles.length()").value(0));
|
||||
}
|
||||
}
|
||||
39
src/test/java/se/urmo/hemhub/web/MeControllerIT.java
Normal file
39
src/test/java/se/urmo/hemhub/web/MeControllerIT.java
Normal file
@ -0,0 +1,39 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class MeControllerIT {
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void me_withToken_200_andContainsClaims() throws Exception {
|
||||
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||
j.subject("test-user");
|
||||
j.claim("email", "test@example.com");
|
||||
j.claim("preferred_username", "test");
|
||||
j.claim("household_id", "H-ANDERSSON");
|
||||
j.claim("realm_access", Map.of("roles", List.of("MEMBER")));
|
||||
})))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.preferred_username").value("test"))
|
||||
.andExpect(jsonPath("$.householdId").value("H-ANDERSSON"))
|
||||
.andExpect(jsonPath("$.roles[0]").value("MEMBER"));
|
||||
}
|
||||
}
|
||||
30
src/test/java/se/urmo/hemhub/web/PublicControllerIT.java
Normal file
30
src/test/java/se/urmo/hemhub/web/PublicControllerIT.java
Normal file
@ -0,0 +1,30 @@
|
||||
package se.urmo.hemhub.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class PublicControllerIT {
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void publicInfo_ok() throws Exception {
|
||||
mvc.perform(get("/public/info"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.name").value("HemHub API"))
|
||||
.andExpect(jsonPath("$.version").exists())
|
||||
.andExpect(jsonPath("$.timestamp").exists());
|
||||
}
|
||||
}
|
||||
|
||||
17
src/test/java/se/urmo/hemhub/web/PublicControllerTest.java
Normal file
17
src/test/java/se/urmo/hemhub/web/PublicControllerTest.java
Normal file
@ -0,0 +1,17 @@
|
||||
package se.urmo.hemhub.web;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PublicControllerTest {
|
||||
|
||||
@Test
|
||||
void info_returnsExpectedKeys() {
|
||||
var controller = new PublicController("test");
|
||||
var result = controller.info();
|
||||
assertThat(result).containsKeys("name", "version", "timestamp");
|
||||
assertThat(result.get("name")).isEqualTo("HemHub API");
|
||||
}
|
||||
}
|
||||
11
src/test/resources/application-test.yml
Normal file
11
src/test/resources/application-test.yml
Normal file
@ -0,0 +1,11 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:hemhub;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
flyway:
|
||||
enabled: false
|
||||
Reference in New Issue
Block a user