SpringBot security third look: Jwt and OAuth2
Introduction
This is the third post and will be dedicated to OAuth2. There are two separate parts of OAuth2 in Spring Security, OAuth2 Resource Server and OAuth2 Client. Based on the OAuth2 RFC, OAuth2 resource server is the authorization server, sometimes also the resource owner and the resource provider if resources are hosted in the same server. On the other hand, OAuth2 client makes request to restricted resources, it often happens when you make requests to the resources you hosted in cloud providers, such as AWS and GCP.
OAuth2 Resource Server
Based on the official docs and knowledge we learned from previous post, we know that in order to validate user-provided credentials (username/password or token), we need to
- configure an AuthenticationFilter class, like UsernamePasswordAuthenticationFilter registered by formLogin(withDefaults()), to transform credentials into an AuthenticationToken object
- Pass the AuthenticationToken to a ProviderManager that contains many AuthenticationProviders, e.g. DaoAuthenticationProvider. Loop over providers to see if the object can be processed.
- If authentication succeeds, rest providers do not need to be run. A new AuthenticationToken with proper authorities will be created. Otherwise, if authentication fails, the application will throw an AuthenticationException that handled by ExceptionTranslationFilter, that decided which actions to take.
BearerTokenAuthenticationFilter
Similar to UsernamePasswordAuthenticationFilter, BearerTokenAuthenticationFilter is used to handle Http requests that has an Authorization header.
The filter is configured using http.oauth2ResourceServer() method. The oauth2ResourceServer() method provides two configurations, Jwt and OpaqueToken.
- Jwt provides readable content. Client can understand information once decode the token.
- OpaqueToken is only intended to be understood by the issuer. It can have proprietary information that is not for public uses.
Here we only consider Jwt. To set up a minimal configuration for Jwt for. You need a default JwtConfigure and a JwtDecoder bean. Otherwise, application will not start. The code came from spring sample GitHub
1 |
|
The documentation in spring.io explains the process pretty well And I just want to fill in with more details
The default implementation of JwtDecoder.decode() uses DefaultJWTProcessor to verity that if the Jwt header contains “alg: RS256” and the payload contains “sub”. So generally speaking, the default BearerTokenAuthenticationFilter will allow any Jwt tokens with those two constrains applied.
Coding session
In this session, instead of using Nimbus, I want to use io.jsonwebtoken JJWT library to build jwt encoder and decoder. Also, I will take note of some experiments.
JwtDecoder
As described top, to allow spring application to run with oauth2ResourceServer(), we need a JwtDecoder Bean. So let’s implement one using the jjwt library.
1 |
|
To explain the code above:
- We use JJwt library to transform token value into Jws claim set, verify that Jws contains “alg” in header and “sub” in payload.
claimSetConverter
(code) is a set of value converters to convert user provided claims into values required by spring security.- For example, “exp” and “iat” fields both have Long type values, but spring’s Jwt requires both to be Instant type values.
MappedJwtClaimSetConverter::convertInstant
is used to convert Long to Instant.
- For example, “exp” and “iat” fields both have Long type values, but spring’s Jwt requires both to be Instant type values.
jwtValidator
is a set of validation checks that make sure Jwt is valid.- For example, the default validator,
JwtTimestampValidator
code, checks if the token has expired or not.
- For example, the default validator,
- validateJwt() and getJwtValidationExceptionMessage() methods are directly borrowed from NimbusJwtDecoder.java.
Now the application should run and behave the same as the example code on top.
JwtEncoder
Next, I want to issue a Jws to users who has logged in. I need to implement a JwtEncoder bean.
1 |
|
There is no extra work done here because all header and claims information are provided by the caller. The encoder will only set the “alg” field in header.
Retrieve result from BearerTokenAuthenticationFilter
The original purpose of this section is to implement a repository service for Jwt tokens. But later on, I realize that it’s a dumb idea to save jwt tokens unless the application needs bookkeeping (write-only). The only thing I can consider of doing is to refresh token after validation, so I rewrite this side as a side note for that.
PostBearerTokenAuthenticationFilter
The idea for this is to add a PostBearerTokenAuthenticationFilter right after BearerTokenAuthenticationFilter. So the code will be
1 |
|
The only problem is how to retrieve the authenticationResult from BearerTokenAuthenticationFilter. To do that, I realize that after Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
, the authenticationResult is put in a SecurityContext object and saved in two places at the same time:
- SecurityContextHolderStrategy: save context in thread-local per request (default) or globally in application.
- SecurityContextRepository: save context in the http’s request attributes, keyed by
RequestAttributeSecurityContextRepository.class.getName() .concat(".SPRING_SECURITY_CONTEXT");
.
Hence, to modify the AuthenticationResult that came from BearerTokenAuthenticationFilter:
1 | public class PostBearerTokenAuthenticationFilter extends OncePerRequestFilter { |
This method can apply to any object that you want to share within the same request.
Customize JwtConfigurer
This is just my thought, I have not actually fully implemented one yet. For code below,
1 |
|
I can replaceconfigure.jwt(withDefaults())
toconfigure.jwt(jwtConfigurer -> jwtConfigurer.authenticationManager(new ProviderManager(new JwtAuthenticationProvider(new JJwtDecoder(this.publicKey)))))
.
In that case, I can do some extra work
1 | public class MyProviderManager extends ProviderManager { |
Unlike ProviderManager, the implementation of MyProviderManager will only authenticate the request must when both jwtProvider and postJwtProvider return success.
Things to say
The only thing I feel that is important but yet to try is ObjectPostProcessor class. It seems to me that it is like a wrapper class where you can do some extra initialization and cleanup, but I cannot be certain though.
I think my next post will relate to the spring security’s OAuth client and test connecting to external services like GCP and AWS.