15

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 request and the client_id is the same one used to generate the code. However, with the Spring Security OAuth 2.0 implementation, it appears that basic auth is always required on the /oauth/token endpoint even if the client was never assigned a secret.

It looks like there is support for allowing clients without a secret due to the isSecretRequired() method in the ClientDetails interface. What do I need to do to enable clients without a secret to be authenticated at the /oauth/token URL?

4.1.3. Access Token Request

The client makes a request to the token endpoint by sending the
following parameters using the "application/x-www-form-urlencoded"
format per Appendix B with a character encoding of UTF-8 in the HTTP
request entity-body:

grant_type REQUIRED. Value MUST be set to "authorization_code".

code REQUIRED. The authorization code received from the authorization server.

redirect_uri REQUIRED, if the "redirect_uri" parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical.

client_id REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1.

If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the
client MUST authenticate with the authorization server as described
in Section 3.2.1.

dur
  • 15,689
  • 25
  • 79
  • 125
quintonm
  • 838
  • 1
  • 7
  • 20

7 Answers7

13

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.

dur
  • 15,689
  • 25
  • 79
  • 125
quintonm
  • 838
  • 1
  • 7
  • 20
  • 2
    Still doesn't work for me. When I omit the Authorization header I do get 401 code back. – ECostello Dec 01 '17 at 13:59
  • @ECostello Did you ever figure out why? I'm in the same situation as you. – Sebastiaan van den Broek Jun 26 '18 at 11:11
  • @SebastiaanvandenBroek, is this still an issue for you ? if yes, try to set the secret as null. This means whatever secret or no-secret will be accepted. – Anddo Oct 04 '19 at 21:02
  • I have a very similar situation. However in my case I have multiple clients and some of them uses basic auth while other uses form parameter. Whenever I add allowFormAuthenticationForClients(), the clients that uses basic auth fails to access. Is it possible to promote both methods together? – kundante Dec 03 '19 at 11:24
  • so I spent a whole day only to find a 1 line solution :-/ – Ankit Apr 12 '22 at 19:37
  • however, it still doesn't take parameters from json form but only form-encoded. any solution for that? – Ankit Apr 12 '22 at 19:38
7

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.

ddewaele
  • 22,363
  • 10
  • 69
  • 82
  • 2
    That's what I also thought, but I keep getting 401. The doc of EnableAuthorizationServer also says: "but the Token Endpoint (/oauth/token) will be automatically secured using HTTP Basic * authentication on the client's credentials" – lilalinux Mar 18 '18 at 10:02
  • @lilalinux Did you figure out what the problem was for you? I'm in the same situation. – Sebastiaan van den Broek Jun 26 '18 at 11:14
  • 1
    @SebastiaanvandenBroek Try .secret("{noop}") in the client definition. That should accept an empty password. – lilalinux Jun 26 '18 at 16:39
  • 1
    @lilalinux thanks for your reply, I had similar success encoding an empty password with a standard password encoder, see my answer here. I guess it comes down to the same thing. – Sebastiaan van den Broek Jun 26 '18 at 16:41
1

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);
}
apo
  • 116
  • 7
1

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: enter image description here enter image description here

Test 2: enter image description here

Roger Pc
  • 109
  • 1
  • 4
0

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!

Sebastiaan van den Broek
  • 5,818
  • 7
  • 40
  • 73
0

I use Spring boot 2.5.0 and I do not need the allowFormAuthenticationForClients(). My PasswordEncoder looks like this:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return BCrypt.hashpw(charSequence.toString(), BCrypt.gensalt());
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return BCrypt.checkpw(charSequence.toString(), s);
            }
        };
    }

I generated the secret as empty string through this website

just click on the button "Bcrypt" and take the hash which you will insert into a database.

Peter S.
  • 470
  • 1
  • 7
  • 17
-1

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.