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

How to authenticate resource owner with JWT when sending request 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

My use-case is a bit different from the samples in the new oauth2-authorization-server. Resource owners are not authenticating with a basic auth login form, but are coming to my OAuth2 server with a Bearer token, which is generated by a different service. I need to authenticate them via a JWT Bearer token.

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());
      }
    }