I am having a hard time trying to find an appropriate solution for my use case since I can't seem to find documentation about this. The closest I could find that relates a little to my issue would be this.
Here's a little summary of my situation
All of our Spring Boot microservices are using an OAuth2ResourceServer
configured with our internal provider (let's call it Provider A). That way, only other microservices (configured with an OAuth2 WebClient) or employees can send requests to our microservices. So far, I got this working without any problem.
The problem I am facing is that some endpoints of our microservices require an additional token (Provider B) in order to authorize our customers (eg. to access or modify their data).
As such, all HTTP requests that are sent to our microservices includes an Authorization
header for our Provider A. On the other hand, some of those HTTP requests also have an CUSTOMER_TOKEN
header for our Provider B.
My question
How would one configure Spring Boot so that the OAuth2ResourceServer
support 1 or 2 Jwt tokens from two different issuers? Any pointers or better solutions for this?
What I've attempted
So far, I've tried a couple of things which feels hacky and I suspect I am doing something wrong or I am just not looking in the right direction.
The best solution that I have come up with consists of an OAuth2ResourceServer
configured with a custom Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>>
, a custom AuthenticationManagerResolver
, a custom AccessDeniedHandler
, a custom AuthenticationEntrypoint
, and two JwtDecoder
configured with their respective JWK Set URIs and Issuer URIs:
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
return httpSecurity.csrf()
.disable()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(WHITELIST)
.permitAll()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer(configurer ->
configurer
.authenticationManagerResolver(authenticationManagerResolver())
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
)
.build();
}
@Bean
public AccessDeniedHandler accessDeniedHandler(){
return (request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
PrintWriter writer = response.getWriter();
writer.write("\"message\": \"access denied\"");
writer.flush();
};
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return (request, response, authenticationException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
PrintWriter writer = response.getWriter();
writer.write("\"message\": \"unauthorized\"");
writer.flush();
};
}
@Bean
protected AuthenticationManagerResolver<HttpServletRequest>
authenticationManagerResolver() {
return request ->
authentication -> {
List<GrantedAuthority> authorities = new ArrayList<>(); // merged authorities
Map<String, Object> attributes = new HashMap<>(); // merged attributes
Authentication providerA = authenticate(request, OAuth2Provider.PROVIDER_A, authorities, attributes);
if (request.getHeader(OAuth2Provider.PROVIDER_B.getHeader()) == null) {
// If Provider B token is not in the request, the user won't have the appropriate role
// to access endpoints annotated with @RoleAllowed(CUSTOMER_ROLE)
return providerA;
}
return authenticate(request, OAuth2Provider.PROVIDER_B, authorities, attributes);
};
}
public final Authentication authenticate(
HttpServletRequest request,
OAuth2Provider oAuth2Provider,
List<GrantedAuthority> authorities,
Map<String, Object> attributes) {
Jwt jwt = decodeAccessToken(request, oAuth2Provider); // JwtDecoder + error handling
authorities.add(new SimpleGrantedAuthority(oAuth2Provider.getRole()));
authorities.addAll(getTokenAuthorities(jwt));
Map<String, Object> providerAttributes = new HashMap<>(jwt.getClaims());
providerAttributes.put("token", jwt);
attributes.put(oAuth2Provider.getName(), providerAttributes);
OAuth2AuthenticatedPrincipal principal =
new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities);
OAuth2AccessToken accessToken =
new OAuth2AccessToken(
TokenType.BEARER, jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt());
return new BearerTokenAuthentication(principal, accessToken, authorities);
}
This solution works, per se, but feels hacky to me since I need to manually reply with 401 or 403, which is not really a good sign when dealing with security. Also HttpServletRequest
seems to have been dropped from Spring 3.x, which kind of amplifies my feeling of hackyness and the possibility of better solutions.