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-
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
https://springone.io/authorized?code=TfzC56cc7xwa0wS-O0VvMm1k6kOhYchOcj7sW_pXyeEaRIvw9V6N5YuXoeqwQka1Cvf0ZY9EzGg0dM9zlCXLPYU3q7_T9KVsuc1_sGTV7XBxChPxtq1VRoxuuORfg3Zx
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.
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.