spring-securityoauth-2.0migrationspring-security-oauth2spring-authorization-server

How to authenticate resource owner with JWT token on Spring Authorization Server for requests to /oauth2/authorize?


I'm migrating from spring-boot 2.7.18 to 3.3.3 and moving from spring-security-oauth2 to spring-boot-starter-oauth2-authorization-server

UPDATE:

My usecase: I have gateway service which is sitting between outside world and private network. This gateway existed before authorization server and it's part of system architecture. Basically it intercepts every request under for example https://example.com domain, now when my public clients call https://example.com/oauth2/authorize gateway service will intercept request, it will figure out that user doesn't have active session and then it will redirect user to login page hosted on domain which is not the same as domain for oauth2 server. After user logs in successfully gateway service will redirect to initially requested url https://example.com/oauth2/authorize and will add Bearer token to request headers.

Next on my oauth2 server I need to validate JWT Bearer token and if it's valid let the authorization library to execute next thing Provider/filter in the chain.

my oauth2-server had this defaultSecurityFilterChain config

            http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .antMatcher("/oauth/authorize")
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .addFilterAfter(new CustomJwtAuthenticationFilter(secret), BasicAuthenticationFilter.class)
                .addFilterBefore(new CustomHSTSFilter(), CustomJwtAuthenticationFilter.class)

which worked fine with old spring-security-oauth2 library

Now since a lot of things changed in the new spring security and oauth2-authorization-server I have changed defaultSecurityFilterChain config to this

        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorize) -> authorize
                         .requestMatchers("/oauth2/authorize","/hello").authenticated())
                .addFilterAfter(new CustomJwtAuthenticationFilter(secret), BasicAuthenticationFilter.class)
                .addFilterBefore(new CustomHSTSFilter(), CustomJwtAuthenticationFilter.class);

Unfortunately this doesn't work, and I would appreciate if someone can explain how should I configure security filter chain to work.

My /hello endpoint which I defined is working fine, but when I send request to GET /oauth2/authorize?client_id=.... I get 403 Pre-authenticated entry point called. Rejecting access I can see im my logs that my CustomJwtAuthenticationFilter is invoked and GET /oauth2/authorize?client_id=.... gets secured BUT then it tries to secure GET /error?client_id=... and it fails in AuthorizationFilter I get Access Denied with message Pre-authenticated entry point called. Rejecting access.

Does anyone have an idea what is wrong in my config, and am I missing?

I have tried to add .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll() in my defaultSecurityFilterChain then /error gets Secured but I get 404 without any message.

I have also tried to add .requestMatchers("/error").permitAll() with same 404 result

Also I have tried changing my authorizationServerSecurityFilterChain with adding custom authorizationRequestConverter on authorizationEndpoint but I found myself writing more complex code than it should be and figured out that this is probably not a way to implement this.


Solution

  • Section 3.1 of the OAuth 2 specification requires for the authorization endpoint:

    The authorization server MUST first authenticate the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, passkey, federated login, or by using an established session) is beyond the scope of this specification.

    The example in the Spring Authorization Server documentation redirects to a login form to submit a username and password, but you want to authenticate by a bearer token instead. The Spring Authorization Server developers suggest you can configure a custom AuthenticationConverter in the authorizationRequestConverter of the authorizationEndpoint:

    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
        .authorizationEndpoint(endpoint -> endpoint
            .authorizationRequestConverter(customAuthorizationRequestConverter))
    

    The custom AuthenticationConverter verifies the bearer token and gets the principal from the bearer token:

    @RequiredArgsConstructor
    public class CustomAuthorizationRequestConverter implements AuthenticationConverter {
    
      private static final OAuth2AuthorizationCodeRequestAuthenticationConverter DELEGATE =
          new OAuth2AuthorizationCodeRequestAuthenticationConverter();
      private static final DefaultBearerTokenResolver BEARER_TOKEN_RESOLVER =
          new DefaultBearerTokenResolver();
      private static final JwtGrantedAuthoritiesConverter GRANTED_AUTHORITIES_CONVERTER =
          new JwtGrantedAuthoritiesConverter();
    
      private final JwtDecoder jwtDecoder;
    
      private Jwt getJwt(String encoded) {
        try {
          return jwtDecoder.decode(encoded);
        } catch (JwtException e) {
          throw new OAuth2AuthorizationCodeRequestAuthenticationException(
              new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, e.getMessage(), null), e, null);
        }
      }
    
      @Override
      public Authentication convert(HttpServletRequest request) {
        String accessTokenValue = BEARER_TOKEN_RESOLVER.resolve(request);
        if (accessTokenValue == null) {
          return null;
        }
    
        var authentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) DELEGATE.convert(request);
        if (authentication == null) {
          return null;
        }
    
        Jwt jwt = getJwt(accessTokenValue);
        String username = jwt.getClaimAsString(JwtClaimNames.SUB);
        Collection<GrantedAuthority> authorities = GRANTED_AUTHORITIES_CONVERTER.convert(jwt);
        var principal = new UsernamePasswordAuthenticationToken(username, null, authorities);
    
        return new OAuth2AuthorizationCodeRequestAuthenticationToken(
            authentication.getAuthorizationUri(),
            authentication.getClientId(),
            principal,
            authentication.getRedirectUri(),
            authentication.getState(),
            authentication.getScopes(),
            authentication.getAdditionalParameters());
      }
    }