ayush
ayush

Reputation: 694

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.

Upvotes: 4

Views: 4013

Answers (5)

Abhishek Shah
Abhishek Shah

Reputation: 77

A correct sample curl request is below. I am using the springboot version 3.3.3

 curl -v -X POST 'http://localhost:8080/oauth2/token' \
--header "Authorization: Basic Y2xpZW50OnNlY3JldA==" \
--header "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=client" \
-d "redirect_uri=http://localhost:8080/authorized" \
-d "grant_type=authorization_code" \
-d "code=anc3tK0wCrI6zer4BGesAz6xJlzox-LjrgdjnkJMGlv9j_w5kHBIQNw2RfoZLil0ouBJZSPj_hiSG7sH4qWlYuemLrjyepk6HF3Z4hPC-3kjSzcQ-Mt9cLqmG5cR5niu"

Authorization header has the base64 encoded client credentials:

echo -n "client:secret" | base64
Y2xpZW50OnNlY3JldA==

Upvotes: 0

Richard RJUK
Richard RJUK

Reputation: 523

For those who are using spring-boot 3.3.1

plugins {
    id 'org.springframework.boot' version '3.3.1'
    id 'io.spring.dependency-management' version '1.1.5'
    id 'java'
}

1. Set body to 'x-www-form-urlencoded' enter image description here

2. Auth to basic auth

enter image description here

Upvotes: 1

Steve Riesenberg
Steve Riesenberg

Reputation: 6043

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.

Upvotes: 3

Samal
Samal

Reputation: 61

I am using Spring boot version 3.1.7 and faced the same issue.

I figured out that sending the data as query parameters is not accepted in the specified version.

Instead of sending the request data as query parameters, we need to send the data through form parameters. Sending the request with all query parameters as form parameters in the body section resolved the issue for me.

You can compare the response by using the 'raw' and 'x-www-form-urlencoded' tabs in the postman tool.

Postman tool - comparison in sending the data

Upvotes: 6

Richard
Richard

Reputation: 31

I Just faced this same issue and found no solution to the problem. I had to downgrade my spring-boot version from 3.2.1 to 3.1.0 before I obtained an access token.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.4'
}
  1. Create a GET request to oauth2/authorize-

http://localhost:9090/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://localhost:9090/redirect/authorized&code_challenge=OaWJmipPOKmE66leDkUVMs8V9Iy8l6gg0NBsRFzgAeg&code_challenge_method=S256

  1. After providing the login username and password on the default login form displayed on the browser, you are redirected to the specified redirect url-

http://localhost:9090/redirect/authorized?code=OspZOqTvauO59fLZm7Z7PFO10vzznxpH9TcIhaXIcVshOmI5f_O-oDv4zej7WBS8Jq_USUAm8BgkC-P0vk8mRXUhYTbIwrwmYN3C6hnjP4rNnpghf9MzlyIs8KZGOFgX

  1. Next, use the code obtained from the authorization server to request an access token like so:

http://localhost:9090/oauth2/token?client_id=client&redirect_uri=http://localhost:9090/redirect/authorized&grant_type=authorization_code&code=OspZOqTvauO59fLZm7Z7PFO10vzznxpH9TcIhaXIcVshOmI5f_O-oDv4zej7WBS8Jq_USUAm8BgkC-P0vk8mRXUhYTbIwrwmYN3C6hnjP4rNnpghf9MzlyIs8KZGOFgX&code_verifier=oIoS6DKeU5B3Dccu_QOutW58En8Ycs5tuqk7hv7rY0A

Access Token request on postman

Basic authentication on postman

Access Token on postman

Upvotes: 3

Related Questions