Extending authorization code flows with PKCE

Latest

Auth & identity

August 30, 2023

Author: Stytch Team

Introduction to PKCE

First things first: it’s pronounced “pixie”, not “P-K-C-E”. Now that you sound like you know what you’re talking about – let’s make sure you can back it up.

At a high level, Proof Key for Code Exchange (PKCE) uses dynamically generated secrets to ensure that the user finishing an auth flow is the same user who initiated it – and not a malicious actor. While PKCE was initially designed for native applications like mobile, it has uses elsewhere – but there are a few key caveats to consider when implementing PKCE in your application.

Why do we need PKCE?

To understand PKCE, we first need a quick refresher on the OAuth 2.0 authorization code flow  – the gold standard protocol for allowing an app (the Client) to access another system (the Authorization Server) on behalf of an end user using a short-lived code, or token. 

Clients need to register with the Authorization Server in advance and receive a client_id and client_secret that they will send with their request to the Authorization Server – sort of like their username and password.

The authorization code flow is broken into two main steps:

1. The authorization request

  1. The Client redirects the user to the Authorization Server, and includes their client_id to identify the origin of the request, as well as additional details such as  the scope of information they are requesting  and a redirect_url
  2. Once the user has granted access, the Authorization Server redirects the user back to the provided redirect_url along with an authorization code.

2. The token request

  1. Once the Client receives the response and the code, they can call the Authorization Server again – this time with both their client_id and client_secret as well as the authorization code returned from the initial request. If successful, this request exchanges the code for an access_token that actually grants the Client the requested access

The reason this is a two-step flow is that the initial request is done via a browser redirect of the user, which doesn’t offer a secure way to authenticate that the request is coming from a legitimate Client. While there are ways that a hacker could intercept the returned authorization code in this browser-driven portion of the exchange, the code itself is useless without the client_secret. Once your frontend receives the code, it can pass it along to your backend, which is capable of making secure, direct requests to the Authorization Server with both the authorization code and the client credentials.

If you’re not a mobile engineer, you’re probably thinking “great, problem solved!” But if you have worked on “public clients” – like native mobile apps or single-page apps (SPAs) – you know that keeping a secret isn’t quite that simple. These apps are considered public clients because the source code of these apps can be viewed by anyone either directly (SPAs) or via decompiling (native apps), which makes it impossible to securely store the client_secret that they need to authenticate during the authorization code flow. To make matters worse, native applications also rely on the operating system to redirect the user back to the application – which is often done via native deeplinks (e.g. myapp://deeplink). However, native deeplinks are not actually uniquely registered to the application and can be easily spoofed to steal the authorization code.

So now you’ve got a client secret you can’t secure, and an authorization code that is easy to intercept. What do you do? Enter PKCE.

How does PKCE work?

The root of our problem is the fact that public clients can’t keep a secret. But what if instead of a single, static secret that needs to be loaded into the app’s source code, we had a dynamically generated secret?

PKCE does just that, by introducing three new fields:

  1. code_verifier: our dynamic secret – a cryptographically random string generated by the Client
  2. code_challenge_method: a hash method (typically SHA256)
  3. code_challenge: the hash of the code_verifier, hashed based on the specified code_challenge_method

These fields are used throughout the standard authorization code flow to prove possession of the dynamic secret and ensure that the flow is finished by the same client who initiated it:

1. The authorization request

  1. The Client includes the code_challenge and code_challenge_method in their authorization request
  2. The Authorization Server stores these values, before returning the authorization code as normal

2. The token request

  1. The Client includes the code_verifier in their token request
  2. The Authorization Server authenticates the request by taking the code_verifier, hashing it according to the code_challenge_method and ensuring that it matches the code_challenge included in the initial request

These small changes dramatically mitigate the risks we laid out earlier by introducing a secret that cannot be pulled from the source code of public apps, and providing a way to guarantee that the client issuing the token request is the same as the client that initiated the start of the flow.

When should you add PKCE?

You can use PKCE to provide more security for your end users any time you have a two-step authorization flow, where the steps are connected via a short-lived token. This applies to the traditional OAuth 2.0 authorization code flow that it was designed for, with use cases such as “Sign in with Google” – but it could also be leveraged to improve the security of other two-step, token-based authentication flows, such as password resets or magic links. While PKCE was designed specifically to address the security issues of public apps, it still provides an extra layer of security for normal web applications to protect against other forms of token interception.

In short: PKCE is an excellent addition to a lot of flows. However, there are some situations where PKCE is more critical than others – and also a few situations where PKCE may cause issues for your end users.

Critical

If you are using native deeplinks to power redirects back to your application, it is absolutely critical that you use PKCE. As mentioned earlier, native deeplinks are not actually uniquely registered to a given application, so can be spoofed to steal the user’s access token. As the React Native docs put it, “deep links are not secure and you should never send any sensitive data in them.”

At Stytch, we enforce the use of PKCE for our customers across all of our two-part authentication flows – OAuth, magic links and email-based password resets – if we detect that the triggering request contains a deeplink (in other words, any protocol that isn’t http or https). (And, if you’re using our SDK for both sending and authenticating a magic link, we will take care of generating and sending the code_verifier / code_challenge for you!).

Strongly Recommended

Even if you are not using native deeplinks, it’s still strongly recommended that you leverage PKCE for mobile flows or other public app use cases. 

Regardless of your application type, adding PKCE to your standard OAuth flows like ““Sign in with Google” is also strongly recommended – as it adds extra security without any downsides.

Recommended, with caveats

As mentioned earlier, while not strictly the OAuth 2.0 authorization code flow, most implementations of email-based token auth flows, like magic links and password reset requests, can also leverage the foundations of PKCE to add additional security to their flows!

However, before you add PKCE to these flows, it’s important to call out that there are certain legitimate use cases where the link driving an email-based flow will be opened in a different context than where the link was requested – which is by design prevented by PKCE.

The most common cause of this is the use of a mobile WebView. For example, imagine a user is on the Twitter mobile app and clicks on a link to your application, opening a mobile WebView of your app, where they then request to log in via magic link.  When the user goes to their email app and clicks on the magic link, they will be redirected to your application in their native mobile browser app – not the Twitter WebView they were in when they initiated the request. The browser app they end up in doesn’t know the code_verifier needed to complete the request, and as a result the user’s authentication attempt will fail.

If your application contains sensitive information and has a high risk profile, Stytch strongly recommends still using PKCE and simply asking end users to retry the request from their native mobile browser, which will allow them to both initiate and complete the flow from a consistent client.

For applications without sensitive content, and whose users will often be interacting with the app via another app’s mobile WebView (e.g. news publications, eCommerce sites) PKCE might not be the right fit for your web implementation. In this case, we recommend adding a secondary auth factor such as SMS or WebAuthn for additional security. You can read more details on best practices for adding PKCE to magic link login flows in the Stytch docs.

Isn’t there a simpler option?

Depending on your research, you may have also heard reference to the OAuth 2.0 implicit flow being used for public apps – and at this point, you might be thinking it sounds a lot more straightforward than the PKCE protected authorization code flow.

You’d be correct – it is simpler, but it’s also incredibly insecure and essentially should be considered deprecated as a protocol. The implicit flow solved the lack of secrets in public apps by simply cutting out the whole authorization code exchange, and just directly returning the access_token as a URL fragment of the authorization redirect. This exposes the access token to all sorts of vulnerabilities and for any modern application that supports cross-origin requests (CORS) there is no reason to choose the implicit flow over the more secure authorization code flow combined with PKCE.

Conclusion

If you have a public client (mobile, serverless or single-page), PKCE is an absolute necessity, particularly if you use native deeplinking. However, PKCE can also help strengthen the security of private clients, and at Stytch we strongly recommend adding it to any authorization code flowin order to provide maximum protection for both you and your customers. 

SHARE

Get started with Stytch