spring-bootspring-securityoauth-2.0openid-connectspring-authorization-server

unsupported_grant_type error for authorization_code grant type: Spring Security OAuth2


I am trying to implement the OAuth2 Authorization Server with OpenID Connect, using Spring Security. For this, I am using the authorization code flow with refresh token and JWT. Here is my configuration code-

@Configuration
public class SecurityConfig {

  @Bean
  @Order(1)
  public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
        .authorizationEndpoint(
            a -> a.authenticationProviders(getAuthorizationEndpointProviders()))
        .oidc(Customizer.withDefaults());

    http.exceptionHandling(
        e -> e.authenticationEntryPoint(
            new LoginUrlAuthenticationEntryPoint("/login")));

    return http.build();
  }

  private Consumer<List<AuthenticationProvider>> getAuthorizationEndpointProviders() {
    return providers -> {
      for (AuthenticationProvider p : providers) {
        if (p instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider x) {
          x.setAuthenticationValidator(new CustomRedirectUriValidator());
        }
      }
    };
  }

  @Bean
  @Order(2)
  public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
    http.formLogin()
        .and()
        .authorizeHttpRequests().anyRequest().authenticated();

    return http.build();
  }

  @Bean
  public UserDetailsService userDetailsService() {
    var u1 = User.withUsername("user")
        .password("password")
        .authorities("read")
        .build();

    return new InMemoryUserDetailsManager(u1);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient r1 = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("client")
        .clientSecret("secret")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .scope(OidcScopes.OPENID)
        .scope(OidcScopes.PROFILE)
        .redirectUri("https://springone.io/authorized")
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .tokenSettings(
            TokenSettings.builder()
                .accessTokenFormat(OAuth2TokenFormat.REFERENCE) 
                .accessTokenTimeToLive(Duration.ofSeconds(900))
                .build())
        .build();

    return new InMemoryRegisteredClientRepository(r1);
  }

  @Bean
  public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder()
        .build();
  }

  @Bean
  public JWKSource<SecurityContext> jwkSource() throws Exception {
    KeyPairGenerator kg = KeyPairGenerator.getInstance("RSA");
    kg.initialize(2048);
    KeyPair kp = kg.generateKeyPair();

    RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate();

    RSAKey key = new RSAKey.Builder(publicKey)
        .privateKey(privateKey)
        .keyID(UUID.randomUUID().toString())
        .build();

    JWKSet set = new JWKSet(key);
    return new ImmutableJWKSet(set);
  }

  @Bean
  public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
    return context -> {
      context.getClaims().claim("test", "test");
    };
  }

}

The CustomRedirectUrlValidator

import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;

import java.util.function.Consumer;

public class CustomRedirectUriValidator implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {

    @Override
    public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext context) {
        OAuth2AuthorizationCodeRequestAuthenticationToken a = context.getAuthentication();
        RegisteredClient registeredClient = context.getRegisteredClient();
        String uri = a.getRedirectUri();

        if (!registeredClient.getRedirectUris().contains(uri)) {
            var error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
            throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
        }
    }
}

The PKCE code verifier and code challenge are generated as-

SecureRandom sr = new SecureRandom();

        byte[] code = new byte[32];

        sr.nextBytes(code);

        String codeVerifier = Base64.getUrlEncoder()
                .withoutPadding()
                .encodeToString(code);

        MessageDigest md;
        {
            try {
                md = MessageDigest.getInstance("SHA-256");
                byte[] digested = md.digest(codeVerifier.getBytes());
                String code_challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digested);
                log.info("Code verifier: {}", codeVerifier);
                log.info("Code challenge: {}", code_challenge);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
        }

Now, here are the steps I follow to obtain the access token-

  1. GET request to oauth2/authorize-

http://localhost:8080/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=https://springone.io/authorized&code_challenge=z5f7uuzQ2f0c1CNpuY0UoQE5jSN30YpcxS2s6wmoPq0&code_challenge_method=S256

  1. After providing the login username and password as user and password, the browser redirects to the specified redirection url-

https://springone.io/authorized?code=TfzC56cc7xwa0wS-O0VvMm1k6kOhYchOcj7sW_pXyeEaRIvw9V6N5YuXoeqwQka1Cvf0ZY9EzGg0dM9zlCXLPYU3q7_T9KVsuc1_sGTV7XBxChPxtq1VRoxuuORfg3Zx

  1. Now, using the provided code, POST request to obtain the access token- enter image description here enter image description here

Now, if everything is right I must be able to get the tokens in the response body. But, instead I get the error-

{
    "error_description": "OAuth 2.0 Parameter: grant_type",
    "error": "unsupported_grant_type",
    "error_uri": "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"
}

Now, I checked with Spring Security Grant Types and Spring Security Authorization Server that the grant type authorization_code is valid. But, I can't understand, why am I getting this error? Please help me resolve this.


Solution

  • To add to the other answers, it should be mentioned that issue #1451 Token endpoint should not use query parameters was fixed in versions 0.4.5, 1.1.4, and 1.2.1. This is why you can no longer send query parameters to the token endpoint and should use a POST body with x-www-form-urlencoded to make Token Requests.