Recently, I have received questions asking if Proof-Key for Code Exchange (PKCE) is a replacement for OAuth client authentication and, if so, why do my articles still use a client secret for server-side applications?
Is PKCE a replacement for client authentication? The short answer is: no.
Should you still use client authentication where possible? Yes.
At a high level, PKCE allows the authorization server to validate that the client application exchanging the authorization code is the same client application that requested it and that the authorization code had not been stolen and injected into a different session.
On the other hand, client authentication (e.g. a client secret) allows the authorization server to validate the client application’s identity, proving that it is allowed to swap an authorization code in the first place.
That’s it. That’s the article. For the rest of this article, I’m going to dig into the purpose of PKCE and client authentication, focussing on why one does not replace the other and when you would still use client authentication.
Client Authentication
Client authentication allows your authorization server to verify the identity of a client application when it makes a backchannel request to something like the token endpoint.
Much like a user, client applications can use various methods to authenticate, such as:
- a client secret - a shared secret, a password used by the client application
- a client assertion - e.g. a JWT signed using a private key known only to the client application.
By authenticating the client application, you prove that the requester is the actual client application known to the authorization server and not something trying to impersonate that client. Without client authentication, you cannot fully verify the application’s identity.
Proof-Key for Code Exchange (PKCE)
PKCE allows your authorization server to match a token request to an authorization request when exchanging an authorization code. PKCE enables this using the following flow:
- The client application generates a random value, hashes it, and then sends the hash in the authorization request as the
code_challenge
parameter - The authorization server handles user authentication and consent, issues an authorization code, and remembers the
code_challange
for later - The client application sends the original, unhashed random value in the token request as the
code_verifier
parameter -
The authorization server hashes the
code_verifier
and compares it to thecode_challenge
from the original authorization request- If the values match, the token request succeeds, and the client is issued tokens
- If the values do not match, the token request fails, and the client does not get any tokens
PKCE allows the authorization server to verify that it’s the same entity swapping the authorization code as the one who asked for the code, as only they would know that original, plaintext proof-key. It prevents stolen authorization codes from being injected into the client application by an attacker.
It allows the authorization server to ask, “Is the app that is trying to swap the code for a token the same application that I sent it to? Is it as a result of the correct authorization request?”. If an attacker steals an authorization code, then this verification is vital. Client authentication alone wouldn’t help you here.
PKCE on the Server
PKCE was initially designed to prevent code theft on native applications, helping to mitigate the downsides of native apps not being able to keep a secret. However, it turns out that PKCE is also useful for other application types, even client applications that can keep a secret, as it gives the authorization server a method of detecting code theft that it would otherwise never have.
PKCE is now a recommendation for server-side applications in OAuth 2.1, clarified with:
Historic note: Although PKCE [RFC7636] was originally designed as a mechanism to protect native apps, this advice applies to all kinds of OAuth clients, including web applications and other confidential clients.
PKCE vs. OpenID Connect nonce
PKCE is similar to OpenID Connect’s nonce validation, but in this case, it is the authorization server that is doing the validation, preventing the generation of tokens rather than the client application rejecting invalid tokens. You can also use PKCE in pure OAuth flows, rather than relying on the use of OpenID Connect and identity tokens.
For a complete analysis of the differences between PKCE and nonce, check out Daniel Fett’s article on the topic.
“Do I still need a client secret when using PKCE?”
Yes, assuming you can keep a secret.
PKCE helps protect you against various code injection attacks, but PKCE does not replace client authentication.
With PKCE, you prove that the same application is swapping the code as the one who requested it.
With client authentication, you prove that the application is even allowed to swap the code.
PKCE is not a replacement for client secrets. It is a mitigation against stolen authorization codes that is particularly useful when a client application cannot keep a secret. It’s a bit like a Cross-Site Request Forgery (CSRF) token on a login page. The CSRF token allows you to validate that the user submitted the form on a page you created; however, without the user providing their credentials, you would be trusting them on username alone.
Client Authentication with clients that cannot keep a secret
Is there any point to client authentication if a client application cannot keep a secret? For example, a Single Page Application (SPA) running in the browser would need to have the plaintext credentials in the end-users browser, and a mobile app would need to store it on the end-users phone. These are known as public clients, and this is when you would use PKCE without a client secret.
You could embed some client credentials with the approach of “Why make it easy for them? It’s another hurdle for the attacker”, but when it’s the same credentials across all instances of that client application, then I would say the benefits are negligible. It’s unlikely that anything will go seriously wrong if you do this; just don’t trust it on its own and accept that you’ll have pentesters and bug bounty hunters pointing it out to you every 5 minutes.
A client secret specific to that instance of the client application would be better. You could generate a secret as part of a bootstrapping process such as dynamic client registration. In this case, the public client becomes a credentialed client, a client that has a secret but who cannot be trusted based on the secret alone.
It’s not uncommon to have the OAuth functionality delegated onto a secure server for these kinds of applications. For example, by using a Backend for Frontend with SPAs or token exchange for code swaps on mobile apps when PKCE is not supported.