Hello I'm struggling with mocking a JWT token. I'm using JDK 18 and Spring Boot 3 and I'm using Keycloak as openid server to deliver the token to the front and it's send as Bearer token to the backend to do authenticated request.
I also use OpenApi to generate my API code but to simplify I've made a test without it.
Here's my pom dependencies.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc-test</artifactId>
<version>7.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
</dependencies>
main application.properties
server.servlet.context-path=/api
server.port=8080
spring.datasource.url = jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
#spring.jpa.hibernate.ddl-auto=update
spring.data.rest.detection-strategy=annotated
spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop
# Custom H2 Console URL
spring.h2.console.path=/h2
spring.sql.init.mode=embedded
spring.sql.init.platform=h2
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operationsSorter=method
springdoc.api-docs.path=/api-docs
application.initdb=true
origins: http://localhost:8080
issuer: http://localhost:8888/realms/apptest
com.c4-soft.springaddons.oidc.ops[0].iss=${issuer}
com.c4-soft.springaddons.oidc.ops[0].username-claim=preferred_username
com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles
com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_
com.c4-soft.springaddons.oidc.ops[0].authorities[1].path=$.resource_access.*.roles
com.c4-soft.springaddons.oidc.ops[0].authorities[1].prefix=ROLE_CLIENT_
com.c4-soft.springaddons.oidc.resourceserver.cors[0].path=/**
com.c4-soft.springaddons.oidc.resourceserver.cors[0].allowed-origin-patterns=${origins}
the application.properties in test is basically the same except com.c4-soft.springaddons.oidc
TestController
@RestController
@RequestMapping("test")
public class TestConroller {
@GetMapping("/")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<String> test() {
return ResponseEntity.ok("ok");
}
}
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockJwtUserSecurityContextFactory.class, setupBefore = TestExecutionEvent.TEST_EXECUTION)
public @interface WithMockJwtUser {
long value() default 1L;
String username() default "test";
String email() default "test@test.fr";
String[] roles() default {"ROLE_USER"};
}
WithMockJwtUserSecurityContextFactory
public class WithMockJwtUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockJwtUser> {
@Override
public SecurityContext createSecurityContext(WithMockJwtUser customUser) {
Instant issuedAt = Instant.now();
Instant expireAt = issuedAt.plus(1, ChronoUnit.HOURS);
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.header("typ", "JWT")
.issuedAt(issuedAt)
.expiresAt(expireAt)
.claim("sub", customUser.username())
.claim("email_verified",false)
.claim("iss", "http://localhost:8888/realms/apptest")
.claim("typ","Bearer")
.claim("preferred_username", customUser.username())
.claim("given_name", "Test")
.claim("sid", "98aeed08-cfd3-4c59-96d6-9a2a7ec658d2")
.claim("session_state", "98aeed08-cfd3-4c59-96d6-9a2a7ec658d2")
.claim("acr", 1)
.claim("azp", "login-app")
.claim("scope", "profile email")
.claim("exp", expireAt)
.claim("iat", issuedAt)
.claim("jti", "ad071b05-68af-4d05-a58e-e71277511c8f")
.claim("name", "Test test")
.claim("family_name", "test")
.claim("email", customUser.email())
.claim("realm_access=", Map.of("roles",new String[]{"USER"}))
.build();
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(customUser.roles());
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
token.setDetails(new WebAuthenticationDetails("127.0.0.1", null));
context.setAuthentication(token);
return context;
}
}
The test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = TestApiApplication.class)
public class UserControllerIntTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
//@WithMockUser("user")
@WithMockJwtUser(username="user1")
public void testTest() throws Exception {
//given
// when
ResponseEntity<String> resp = restTemplate
.getForEntity("http://localhost:"+port+ "/api/test", String.class);
assertEquals(HttpStatus.OK, resp.getStatusCode());
/*mockMvc.perform(get(baseUrl+"/curr"))
.andDo(null)
.andExpect(status().isOk());*/
}
When I run the test code we pass throw createSecurityContext and seems to return a valid authentication at the end. But I also noticed that log which may erase my mock. 2023-07-29T10:11:36.713+02:00 DEBUG 2496 --- [o-auto-1-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
I have to notice that Even @WithMockUser doesn't work.
I've already seen this similar question but I didn't found my solution. How to mock JWT authentication in a Spring Boot Unit Test?
Do you have an Idea of what's wrong with my test config ? Thanks in advance.