0

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.

dur
  • 15,689
  • 25
  • 79
  • 125
G.T.
  • 1
  • Why do you add a token from provider A to customer requests? Why don't you just propagate the token issued by provider B and configure your resource servers to accept tokens from both A & B issuers? That way, all you'd have to do is define access control rules based on roles and issuer. – ch4mp Apr 27 '23 at 21:51
  • Hi, thank you for your response. I've taken into consideration your pointer and actually decided to follow something along the lines you mentioned. As such, I am configuring endpoint paths with the appropriate resource server, there's just a long road ahead in this journey ;). – G.T. May 23 '23 at 16:45
  • [Those tutorials](https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials) I wrote could ease your journey – ch4mp May 23 '23 at 18:30

0 Answers0