How to support different JWTs in your Spring Boot application

Introduction
This guide is for you if you face one of the following challenges:
- Maybe you want to allow users to authenticate with OAuth/OpenID Connect, but you also want to allow some “technical user” (such as another service) to access your data. That technical user might not get its JSON Web Token (JWT) signed by your central Identity Provider, but could use some other form of authentication.
- You might also want to migrate from some sort of custom authentication mechanism to the more established and standardized OAuth/OpenID Connect, but you don’t want to make the switch in one large “big bang” and risk locking out some users that haven’t yet registered with the Identity Provider and still use the old way.
In both cases you need to support different JWTs in your application.
Delimitations
We’re going to use Java 11 with Spring Boot 2.3. As OpenID Connect is the current gold standard for user authentication, we’re also going to focus on that protocol.
The goal of this blog post is to guide you in how to configure your Spring Boot Application to support a production-ready state-of-the-art authentication mechanism. I won’t explain JSON Web Tokens (JWT), OAuth or OpenID Connect (OIDC) in detail here, but focus on the actual implementation. If you want to learn more about Identity Federation in general, check out the blog post by Dorian. Some basic understanding of Spring Boot can help following this guide.
Default Spring Boot
If you only need to support OpenID Connect from a single authorization server, you probably won’t need this guide. Spring Boot makes it easy as pie. First, add the Spring Security framework to your dependencies. Then, simply specify the issuer of your JWTs in your application.yml
:
1 2 3 4 5 6 |
spring: security: oauth2: resourceserver: jwt: issuer-uri: https://idp.example.com/issuer |
That’s it. Spring Security will automatically pull the latest keys – in form of a JSON Web Key Set (JWKS) – from the authorization server to validate the signatures of incoming JWTs.
How Spring Security handles authentication
Spring Security reads the Authorization header of an incoming HTTP request to determine if a user has valid authentication.
The value can either be “Basic”, followed by an encoded username:password
value.
In modern web applications however, transferring the user’s credentials on each request is not feasible. You’ll see the value “Bearer” in most cases. This indicates that the user is in possession of some sort of access token instead of a password. Those tokens are usually in the JWT (JSON Web Token) format.
Spring Security passes the incoming request through a so called “filter chain” – which acts just like a water purification filter system (layered sand, gravel, charcoal, etc.).
If any of the filters rejects the request for any reason, the chain is broken and the HTTP request is rejected in its entirety. We are going to focus on the so-called BearerTokenAuthenticationFilter
in this blog post. (Read more about Spring Security’s architecture here.)
The Challenge
Our filter chain is rightfully very strict about rejecting tokens (for example expired ones or incorrectly signed ones), so we could face the problem that our application does not know if a JWT is truly invalid or if it was just signed by another issuer (which we also want to support).
So, how do we tell our application to accept multiple JWTs from different issuers while still rejecting invalid ones?
We essentially have two options now:
- We introduce new endpoints and create a new Spring Security Filter Chain for each mechanism we want to support:
- keep the existing endpoint and don’t change any code:
/products/user?id=ad3f5-92feff-1a22ed3
- add another endpoint and handle every request with a dedicated Filter Chain:
/oauth/products/user?id=ad3f5-92feff-1a22ed3
- you would have to do this for each way to authenticate:
/my-auth/products/user?id=ad3f5-92feff-1a22ed3
Oh, please don’t forget to tell your “OAuth users” to use that new /oauth endpoint …
Or maybe you could place some gateway or proxy in between and do some rewrite magic …
But we don’t want to manage double or even triple the endpoints and neither do we want to have more infrastructure and logic in between the application and the user, right?
(The only advantage of this “breadth” approach is that you wouldn’t need to change any of your existing authentication mechanism code.)
We should rather choose the following option: - keep the existing endpoint and don’t change any code:
- We can also enhance the logic of the current filters (“depth” approach) by adding different
AuthenticationProvider
s to theBearerTokenAuthenticationFilter
. These providers can createAuthentication
objects for Spring’sSecurityContext
.
Let’s Code!
In the scope of this blog post, we’re going to support 3 different ways to authenticate:
- Basic authentication
- OAuth access tokens (in form of JWTs), signed by a standard OpenID Connect (OIDC) authorization server
- “Custom” JWTs signed with some static secret that is shared “out-of-band” with the other party
As Spring Security has a default AuthenticationProvider
already built in for the standard OIDC protocol flow, we only need to implement a provider for our own statically signed JWTs. These JWTs should not have the header field “kid” – which would indicate a Key ID in a dynamic JSON Web Key Set (JWKS). Such dynamic sets of public keys that change over time are used in OpenID Connect.
We check for the presence of this header field before trying to decode the JWT.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class StaticJwtAuthenticationProvider implements AuthenticationProvider { public JwtDecoder jwtDecoder() { // initialize a decoder with a secret shared "out-of-band" SecretKeySpec key = new SecretKeySpec("sup3r-s3cure_secRet".getBytes(StandardCharsets.UTF_8), "HmacSHA256"); return NimbusJwtDecoder.withSecretKey(key).build(); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String kid = null; try { // "DecodedJWT" and "JWT" are from the "java-jwt" library by Auth0 and used for reading the JWT header DecodedJWT jwt = JWT.decode(bearerToken.getToken()); kid = jwt.getKeyId(); } catch (JWTDecodeException exception) { // ... } if (kid == null) { logger.info("JWT header does not contain a JWK Set Key ID. Must be a statically signed token."); logger.info("Trying to authenticate ..."); Jwt jwt = jwtDecoder().decode(((BearerTokenAuthenticationToken) authentication).getToken()); // ... } else { return null; } } } |
In order for a Provider to successfully provide authentication to our app, we need to convert the incoming JWT to an Authentication
object (such as an AbstractAuthenticationToken
). We’ll therefore implement a custom converter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class StaticJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final MyUserDetailsService userDetailsService; @Override public AbstractAuthenticationToken convert(Jwt jwt) { String username = jwt.getClaimAsString("sub"); // in this case the username is in the "sub" claim UserDetails userDetails = userDetailsService.loadUserByUsername(username); Collection authorities = extractAuthorities(jwt); // ... return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), "n/a", authorities); } } |
We also need to create a converter for our standard OpenID Connect access tokens:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class OAuthJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final MyUserDetailsService userDetailsService; @Override public AbstractAuthenticationToken convert(Jwt jwt) { String username = jwt.getClaimAsString("email"); // in this case we take the "email" claim as the username UserDetails userDetails = userDetailsService.loadUserByUsername(username); // ... return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), "n/a", authorities); } } |
Side note: It could make sense to make authentication converters more generic and create a
public abstract class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
and let own converters extend that generic converter.
Finally, we need to wire it all together in our WebSecurityConfiguration
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Configuration @EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { private final MyUserDetailsService myUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/health").permitAll() .anyRequest().authenticated().and() // ... .oauth2ResourceServer().jwt() .jwtAuthenticationConverter(oAuthJwtAuthenticationConverter()); } @Bean OAuthJwtAuthenticationConverter oAuthJwtAuthenticationConverter() { return new OAuthJwtAuthenticationConverter(myUserDetailsService); } @Bean StaticJwtAuthenticationProvider staticJwtAuthenticationProvider() { return new StaticJwtAuthenticationProvider(new StaticJwtAuthenticationConverter(myUserDetailsService)); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(staticJwtAuthenticationProvider()); auth.userDetailsService(myUserDetailsService); // add Basic auth here ... } } |
Phew, that’s it! Your application should now be ready to correctly authenticate users that use different kinds of JWTs.
IMPORTANT: As this code is crucial for the security of your application, you should always run automated tests! You should test valid authentication, but you also need to assure that invalid or expired tokens are correctly rejected.
If you need more assistance implementing OAuth/OpenID Connect or need advice on how this can be applied to your company, check our offering here.
You can find all the code on GitHub: https://github.com/daniel-mader/blog-post-spring-multi-jwt
Multi-tenancy support
With the release of Spring Security 5.2.0, multi-tenancy support was introduced which drastically eases setting up multiple token issuers. You can read about it in the official docs.
Implementing multi-tenancy support might be subject of a follow-up blog post.
Comment article
Recent posts





Comments
Ben
Hi, thank you for this post, I found this extremely helpful, however with the OAuth jwt, how is the jwkseturi passed to the resource server or how does the resource server obtain the keys to validate the incoming token?
afa
Thanks for your question.
You specify the jwt key set uri by setting the corresponding property
spring.security.oauth2.resourceserver.jwt.jwk-set-uri
instead of the issuer-uri in application.properties or application.yml. Then spring security automatically fetches the required public key(s) from the authorization server to validate the jwt. It also caches the public keys as long as a jwt comes along that has a new key id for an unknown public key (then it re-fetches new keys)Aakash
Hi thanks for this article. What will be the purpose of client id and secret is that not required for validation on resource server. How can we accommodate multiple oauth providers such as okta, google etc. and get the tokens validated in consistent manner.
afa
Thanks for your question.
The purpose of the client id and client secret is to authenticate a client to the authorization server when requesting a token. On the server-side, you just have to validate incoming tokens by their signature and expiration time, to achieve this you do not need the client id or the secret.
Regarding your second question, validating tokens from different providers can be done with the multi-tenancy feature of spring security. Check this for implementing this:
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/multitenancy.html#oauth2resourceserver-multitenancy
Aakash
What is the significance of client id and secret for external OAUTH providers such as Okta. Is that required only on the client side (webapp/spa etc.) and not at resource server. How to handle multiple oauth providers here such as okta, google etc.
Gabriel
Thanks for this. It worked with Spring MVC! However I tried to adapt it to Spring Webflux and I just don’t know how. I made this question: https://stackoverflow.com/questions/68688620/using-more-than-one-jwt-decoder-with-spring-webflux-security , if you know how to do it please leave an answer.
Dinneya Charles
Hi, good day.
Trying to implement this tutorial on my project but i dont seem to understand something:
Looking at the StaticKwtAuthenticationProvider.java file, line number 14, i dont understand where the bearerToken identifier is from
help pls
Mader Daniel
Hi, you can find the full code on https://github.com/daniel-mader/blog-post-spring-multi-jwt. Hope that helps!
Binh Thanh Nguyen
Thanks, nice tips