According to the spec, requests for a token using the authorization code grant are not required to be authenticated as long as the client_id
is included in the
Spring allows you to define OAuth2 clients with an empty secret. These can be considered "public" clients, or clients that are unable to keep a secret. Think of Javascript apps, mobile apps, ..... You typically don't want to have client secrets stored there.
As you point out, according to the OAuth2 spec, a token endpoint can opt to not require a secret for these public clients.
So in Spring, simply define an OAuth2 client with an empty secret, and configure it for a limited set of grant types (authorization_code and refresh_token)
The Spring Security implementation token url will accept a token exchange without a client secret for that particular OAuth2 client.
like this:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client").secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("")) //empty
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://www.dev.com")
.scopes("all")
.autoApprove(true);
} @Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
It is ok.
In order to solve the issue see the method loadUserByUsername
of class: org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService
:
if (clientSecret == null || clientSecret.trim().length() == 0) {
clientSecret = this.emptyPassword;
}
Probably in your case emptyPassword
has not been initialized with empty encoded password by your password encoder.
Set missing password encoder in AuthorizationServerConfigurerAdapter
:
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.passwordEncoder(passwordEncoder);
}
This worked for me
@Override
public void configure(AuthorizationServerSecurityConfigurer cfg) throws Exception {
cfg
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
.passwordEncoder(clientPasswordEncoder());
}
@Bean("clientPasswordEncoder")
PasswordEncoder clientPasswordEncoder() {
return new BCryptPasswordEncoder(4);
}
Test 1:
Test 2:
Authenticating the client using the form parameters instead of basic auth is enabled using the allowFormAuthenticationForClients()
method as shown in the code sample below.
class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Override
void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
}
}
The allowFormAuthenticationForClients()
method triggers the addition of the ClientCredentialsTokenEndpointFilter
which allows for authentication via form parameters.
Initially I had a similar setup to the accepted answer, which is definitely a prerequisite to make this work. But what is missing is that you cannot simply set the password to null. You must set it to an empty password, for example like this:
String secret = PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("");
clientDetails.setClientSecret(secret);
If you don't do this, you will still get a 401!