I love automated tests. While I’m not really a TDD fan in most cases (I prefer to write tests after the code is written), I still find them invaluable. They help me to understand the code better, and they help me to refactor it with confidence. But the main point is that I’m usually too lazy to test things manually, and on my personal projects I prefer to dedicate the time I have to writing new features, not testing the old ones. While working on one of my recent personal projects I decided to use JWT for authentication, and I wanted to test it properly in Web layer. So, here’s how I did it.
JWT in Spring Boot
JWT Authentication in SpringBoot usually works out of the box. You just need to add spring-boot-starter-security
dependency to your project, and Spring will take care of the rest. You can find more details in Spring Security documentation. In my case I used Firebase Authentication, but really nothing else then setting up the project in Firebase and configuring Spring Security was required. Any other JWT-compatible provider should work just as well (e.g. Auth0, Okta, etc.)
OAuth2 Resource Server config:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://securetoken.google.com/your_project_id
Also, we need to configure SpringSecurity itself:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
class SecurityConfiguration {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.invoke {
cors { }
csrf { disable() }
authorizeRequests {
authorize("/actuator", permitAll)
authorize("/actuator/**", permitAll)
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {}
}
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
}
return http.build()
}
}
This will make sure all requests to /actuator
endpoints are not authenticated, and all other requests are authenticated using JWT. SpringSecurity will take care of the rest and you can extract the user details from the Authentication
or Principal
objects in your controllers.
Testing JWT
Great thing about testing in Spring is that you can decide how much of the application you want to test. You can test just the controller, or you can test the whole application. I my case I just wanted to test the controller, so I used @WebMvcTest
annotation. For start, here’s the simple sample test:
@WebMvcTest(SampleController::class)
@Import(SecurityConfiguration::class)
@WithMockJwtUser
class SampleControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
}
Looks pretty standard. We just define a @WebMvcTest
for our controller, and we import our SecurityConfiguration
which we defined earlier.
The new component here is @WithMockJwtUser
annotation to mock the JWT user. Here’s the code for it:
@Retention(AnnotationRetention.RUNTIME)
@WithSecurityContext(factory = WithMockJwtUserSecurityContextFactory::class)
annotation class WithMockJwtUser(
val username: String = "user",
val email: String = "user@example.com",
val roles: Array<String> = ["USER"]
)
It’s simple enough - we can define the username, email and roles for our user. But how does it work? Let’s look at the WithMockJwtUserSecurityContextFactory
:
@TestComponent
class WithMockJwtUserSecurityContextFactory : WithSecurityContextFactory<WithMockJwtUser> {
override fun createSecurityContext(annotation: WithMockJwtUser): SecurityContext {
val context: SecurityContext = SecurityContextHolder.createEmptyContext()
// Set headers
val headers = mapOf(
"alg" to "HS256",
"typ" to "JWT"
)
// Set JWT claims for authentication, e.g. "sub", "roles", etc.
val claims: Map<String, Any> = mutableMapOf(
"iss" to "https://mockissuer.com",
"aud" to "audience",
"sub" to annotation.username, // this is an external User ID from Firebase
"name" to annotation.username,
"email" to annotation.email,
"picture" to "https://placekitten.com/96/96",
"roles" to annotation.roles.joinToString(",") { "ROLE_${it.trim()}" }
)
val issuedAt = Instant.now()
val expiresAt = issuedAt.plus(Duration.ofDays(1))
val jwt = Jwt("mockToken", issuedAt, expiresAt, headers, claims)
val authentication = JwtAuthenticationToken(jwt)
authentication.isAuthenticated = true
authentication.details = WebAuthenticationDetails(MockHttpServletRequest())
context.authentication = authentication
return context
}
}
The above implementation is pretty simple. We just create a JWT token with the claims we need, and then we create a JwtAuthenticationToken
with the token we created. The JwtAuthenticationToken
is a standard Spring Security class, and it’s used to authenticate the user. The JwtAuthenticationToken
is then set as the authentication object in the SecurityContext
and returned.
When our controller is called, the JwtAuthenticationToken
will be used to authenticate the user, and we can extract the user details from the Authentication
object.
As we also have properties like roles
in our JWT token, we can use them to test the authorization part of our controller. For example, if we have a method like this:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
fun admin(): String {
return "admin"
}
We can test it like this:
@Test
fun `admin endpoint should return 200`() {
mockMvc.get("/admin") {
accept = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk }
}
}
And that’s it. We can now easily test our JWT authentication and authorization in our controller tests.
Until next time ✌️