8

I am building 2 apps; a front-end, and a back-end.

The back-end will be built using Rails API + Doorkeeper Gem (oauth2 provider) while the front-end will be built using React Native.

Currently, I am using "Client Credentials Grant Flow" which works just fine at the moment. But after researching for a while, this flow shouldn't be used in a client-only app as it exposes the client_secret if ever someone decompiles the app.

I have also read about "Implicit Grant Flow" which only requires client_id. But this flow seems old now?

And according to this: https://auth0.com/docs/api-auth/which-oauth-flow-to-use#is-the-client-a-single-page-app-

It is recommending to use "Authorization Code Grant with PKCE" over "Implicit Grant Flow". I am able to make it work but the problem is that it still needs the client_secret in order to get an access_token, is this how it should be?

Here is the sample flow I am doing:

curl -X POST 'http://localhost:3000/oauth/authorize?client_id=xxxx&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=public&code_challenge=testchallenge&code_challenge_method=plain'

This will give me the following response:

{
    "status": "redirect",
    "redirect_uri": {
        "action": "show",
        "code": "8quZ-EAiKKG2EKnQiSYs3xeFRCgsIwcTbaWNdjnpyFg"
    }
}

And then I will use the code above to get an access token using the request below:

curl -i http://localhost:3000/oauth/token \
  -F grant_type="authorization_code" \
  -F client_id="xxxx" \
  -F client_secret="xxxx" \
  -F code="8quZ-EAiKKG2EKnQiSYs3xeFRCgsIwcTbaWNdjnpyFg" \
  -F redirect_uri="urn:ietf:wg:oauth:2.0:oob" \
  -F code_verifier="testchallenge"

Now here is the problem, in order to exchange code to an access_token I will still be needing the client_secret. How is it different from "Client Credentials Grant Flow" if both will just expose my client_secret?

The above command will return the following:

{
    "access_token": "nQoorBqLxQH4qFpmlx3mGG6Cd_TfX4d3L3gAGOTwrFs",
    "token_type": "Bearer",
    "expires_in": 7200,
    "scope": "public",
    "created_at": 1595517643
}

If I don't include the client_secret in the request here is the response:

{
    "error": "invalid_client",
    "error_description": "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method."
}

So here are my questions:

  1. Do we really need client_secret to get access_token on PKCE flow?
  2. Why is it recommended to use "PKCE Flow" if it will just expose the client_secret?
  3. How is it different from "Client Credentials Grant Flow" which also exposes the client_secret?

Doorkeeper PKCE documentation: https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-PKCE-flow

dcangulo
  • 1,888
  • 1
  • 16
  • 48
  • PKCE does not need a client secret – Evert Jul 23 '20 at 15:39
  • @Evert Does this mean something is wrong with Doorkeeper? As `client_secret` must be present when exchanging `code` with `access_token`. – dcangulo Jul 23 '20 at 15:40
  • My understanding is that that doesn't make a ton of sense for the intent of this flow. `client_secret` is typically optional/not-used here because it's already assumed its 100% public. If doorkeeper requires it, I don't see a ton of risk though *unless* that secret also lets you other things, like using `client_credentials` – Evert Jul 23 '20 at 15:44

3 Answers3

8

Update


In the meantime recommendations have changed (see also Kavindu's answer for references) and I want to make sure this is also emphasized in this answer. Including PKCE as an additional layer of security supplementary to the client secret also makes sense for confidential clients. The client secret allows the authorization server (identity provider) to determine the identity of the client.

Using PKCE allows to make sure that the party trying to exchange the authorization code for the token is the one that actually requested the authorization code in the first place. So to avoid interception scenarios it also makes sense to add PKCE on top even for confidential clients.

But keep in mind that not all identity providers might support using PKCE and a client secret at the same time (yet).

As for the problem stated with Doorkeeper this behaviour might in the meantime also have changed.


Original Answer


Authorization code flow with PKCE was invented for setups where the client is not able to securely protect a secret. So when using authorization code flow with PKCE you don't need a secret or to be more precise a client secret would make no sense.

It seems like what you are experiencing is a Doorkeeper bug (see https://github.com/doorkeeper-gem/doorkeeper/issues/1089). So I'm afraid until they fixed it you will have to use some dummy client secret.

But as long as Doorkeeper implements the rest of the PKCE flow correctly this should not be an issue as this flow is not dependent on any static client secret but instead uses the dynamically created code verifier as you already pointed out.

I'm not sure if I correctly understand what kind of client you are using that handles the login. If it is a SPA or mobile app you should use authorization code flow with PKCE but if you are implementing a "classic" web application where the login is handled in some backend service you should go with the normal authorization code flow using a client secret as backends can be trusted to protect the secret.

By the way, I just checked the code in one of my projects I'm working on where I build some Angular SPA which authenticates via Auth0 as identity provider. I'm using the authorization code flow with PKCE there and I definitely don't send any static client secret (like in the original authorization code flow) to Auth0 because obviously the flow is implemented as intended. So this really has to be an issue with Doorkeeper.

One other thing: I don't know if the requests you provided are just examples but instead of using the method plain for transforming the code verifier into the code challenge I would rather use a secure method such as S256 instead as recommended in the RFC which you referenced in your question.

Andreas Hütter
  • 3,288
  • 1
  • 11
  • 19
  • The last part is not exactly correct. On PKCE you send a (generated) client secret when you first start the login process. The hashed value and the hash algorithm will be sent. Once you get the answer, you get the "code" for "authorization code" flow in the redirect. You use this code with the client id + the generated client secret (unhashed this time) to the server. The server will hash your client secret and compare it with the initial request and if its the same, return you the token and refuse otherwise. – Tseng Nov 10 '22 at 15:19
  • Yes, that's the PKCE specification explained. Can you please elaborate which part of the answer contradicts to that? As a client you transform (hash) the code verifier with the respective method (e.g. should be S256) into a code challenge which you then send along with the method. The client secret you are referring to is called code verifier in the specs. In the last part of the answer I just want to point out that using "plain" as hashing algorithm is not a good idea and I would recommend e.g. S256 as method...See also https://datatracker.ietf.org/doc/html/draft-ietf-oauth-spop#section-3 – Andreas Hütter Nov 10 '22 at 15:39
  • That one: "I'm using the authorization code flow with PKCE there and I definitely don't send any client secret to Auth0 because obviously". While you don't need to send it explicitly, the library still sends it when the token is retrieved as evidence that it's the original application who initialized the login request in the first place. Hence the secret is always sent, but with PKCE its not a static secret but a dynamically (per-login request) generated one, but it's still required for the token endpoint (sure you can turn of in Identity Server 4+ for example but bad idea) – Tseng Nov 15 '22 at 19:09
  • As you can see this was the original answer which I kept for reference ;-) in the updated answer at the top I clarified that in the meantime authorization code with PKCE is also recommended for confidential clients. This means you would send a statically configured client secret and also the dyamically created one (code verifier/challenge). Still it does not make sense to also send the static client secret in addition for non-confidential clients because such a client couldn't keep that one secret anyways. And by the time of the original answer doorkeeper demanded that... – Andreas Hütter Nov 15 '22 at 21:01
  • ...and that's what the original post was all about. I updated the answer to clarify the difference between the static client secret in the original authorization code flow and the dynamically created secret (called code verifier in the standard) which was introduced with PKCE. So there's a difference between these two. – Andreas Hütter Nov 15 '22 at 21:02
3
  1. Do we really need client_secret to get access_token on PKCE flow?

It depends. Originally PKCE was introduced to protect public clients (a client which cannot protect a secret). But recently, with practices, PKCE became a recommendation for authorization code grant (source)

2.1.1. Authorization Code Grant

Clients MUST prevent injection (replay) of authorization codes into
the authorization response by attackers. The use of PKCE [RFC7636]
is RECOMMENDED to this end. The OpenID Connect "nonce" parameter and ID Token Claim [OpenID] MAY be used as well. The PKCE challenge or
OpenID Connect "nonce" MUST be transaction-specific and securely
bound to the client and the user agent in which the transaction was
started.

Note: although PKCE so far was designed as a mechanism to protect
native apps, this advice applies to all kinds of OAuth clients,
including web applications.

  1. Why is it recommended to use "PKCE Flow" if it will just expose the client_secret?

In short, to avoid authorization code replay attacks (spec - introduction). And this happens inside end user's device and not in the transmission of data. TLS is mandatory for OAuth 2.0 token request.

  1. How is it different from "Client Credentials Grant Flow" which also exposes the client_secret?

No grant will expose credentials as token requests are done via TLS.

I think in your case, the client is a confidential client (spec - client types). So I would recommend to check this aspect in authorization server.

Community
  • 1
  • 1
Kavindu Dodanduwa
  • 12,193
  • 3
  • 33
  • 46
1

I am creating a Doorkeeper::Application like this in the Rails console:

Doorkeeper::Application.create :name => 'Test App', :uid => 'xxxx', :secret => 'xxxx', :redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'

It seems like I need to make the Doorkeeper::Application's confidential field to false to be able to get access_token without client_secret.

So the above code will become:

Doorkeeper::Application.create :name => 'Test App', :uid => 'xxxx', :secret => 'xxxx', :redirect_uri => 'urn:ietf:wg:oauth:2.0:oob', :confidential => false

I found the solution in: https://github.com/doorkeeper-gem/doorkeeper/blob/master/spec/requests/flows/authorization_code_spec.rb#L348

dcangulo
  • 1,888
  • 1
  • 16
  • 48