Reputation: 694
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.
Upvotes: 4
Views: 4013
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
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'
2. Auth to basic auth
Upvotes: 1
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
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
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'
}
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
http://localhost:9090/redirect/authorized?code=OspZOqTvauO59fLZm7Z7PFO10vzznxpH9TcIhaXIcVshOmI5f_O-oDv4zej7WBS8Jq_USUAm8BgkC-P0vk8mRXUhYTbIwrwmYN3C6hnjP4rNnpghf9MzlyIs8KZGOFgX
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
Upvotes: 3