Implicit Flow vs. Code Flow with PKCE

Share on facebook
Share on google
Share on twitter
Share on linkedin

If you have read my Angular and OpenID Connect blog post series, you might have seen that I in the last part, when setting up Angular app to use OpenID Connect, went from using implicit flow to use code flow with Proof Key for Code Exchange (PKCE). When the blog post series was initially created (May 2018), using implicit flow in Angular apps was the best practice according to The Internet Engineering Task Force (IETF). Now the new recommendation is to use code flow with (PKCE), as that doesn’t include a static secret but instead generates a dynamic secret, making it possible to use with public clients.

This post will look at the differences with implicit flow and code flow with PKCE and why you should migrate your single page application to code flow with PKCE.

Why use PKCE instead of implicit flow?

You might wonder, why PKCE now is the recommended way of handling authentication in SPAs like Angular apps.

The reason is code flow with PKCE solves some known threats with the implicit flow.

Threat: Interception of the Redirect URI

Since the access token is sent as the fragment (hash) part of the redirect URL (also called the front channel), it will be exposed to an attacker if the redirect is intercepted. Now, of course, it is recommended to always use HTTPS, but even HTTPS can be compromised by man-in-the-middle attacks with methods as corporate SSL certificates to inspect traffic.

This threat is mitigated in code flow by not getting the token response from a redirect but instead a direct request-response HTTP request.

Threat: Access Token Leak in Browser History

The reason why the access token is transported in a hash fragment instead of a query param was in the first place to avoid it being stored in the browser history. Also, using short-lived access token expiration times and instruct browsers to not cache the response is used to minimize the risks of the access token being leaked in the browser history.

Neither of these will eliminate the risk of the access token being leaked in the browser history, as all of this is browser-specific, where some browser might behave differently regardless of these instructions, especially for legacy browsers.

This is mitigated in code flow by doing an HTTP request instead of redirect, as these will not be cached in the browser history.

Threat: Manipulation of Scripts aka cross-site scripting

In case of scripts being modified as part of a man-in-the-middle attack, having the access token in the window.location makes it easy for the attacker to locate the access token in contrast to doing a targeted attack against a code flow client, where the attacker needs to know where the access token is being stored.

This is mitigated in code flow by not storing the access token in the URL but instead stored in local storage under a key that could vary.

Threat: Access Token Leak to Third-Party Scripts

The same concerns, as for the previous threat, applies for third party scripts. When the access token is returned as a fragment in the URL, it is easy for any third-party script to simply do a window.location and get the access token. From here they can just pass this on to the attacker’s server, and voila! Now they have access to everything!

Especially in Angular development where so many NPM libraries are being installed is third-party scripts a serious security vulnerability. The best way to protect against this is to apply the Principle of least privilege meaning that a third party script should not be able to get sensitive information such as OAuth tokens and login credentials or at least, this should be harder.

This is mitigated in code flow by not storing the access token in the URL but instead stored in localhost under a name that could vary.

What are the differences between code flow (with PKCE) and implicit flow?

The differences are that the code flow (with PKCE) uses indirection/backchannel to option the tokens (access and id token) and implicit flow will get it directly on the redirect/front channel.

Now, I have already described the different code flows in OpenID Connect/OAuth2, so the more interesting question is: what is the difference between normal code flow and code flow with PKCE?

When using code flow with PKCE, all the principle of code flow still applies (code returned on authorization request is exchanged for access and/or id token). The PKCE makes this more safe for native and web applications (public clients) by generating a code exchange key, that ensures that the authorization request and the token request is done by the same client (aka not intercepted by a man in the middle). Because web applications can’t store secrets, PKCE allows for creating a secret dynamically at the beginning of the authorization flow as a contrast to the static secret in code flow (can only be used for private/server clients).

 

The complete code flow with PKCE looks like this:

Now, some important differences to note between code flow with and without PKCE is that PKCE simply extends code flow with these 4 steps:

1) Generate code verifier

Before the app begins the authorization request, it will generate the code verifier, a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long. We will use this to generate a code challenge and for the server to verify client identity before issuing tokens in step 4.

2) Use the code verifier to generate the code challenge

The code verifier is used to generate the code challenge. This requires that the device can perform a SHA256 hash. The code challenge is a BASE64 URL encoded string of the SHA256 hash. If the client can’t perform the SHA256 hash it is permitted to use the plain code verifier string as a challenge.

3) Send code_challenge and code_challende_method with the authorization request

The code_challenge and code_challende_method are sent on the authorization request along with the usual parameters in the authorization request. The code_challenge is either the code verifier (if the device can’t perform the hash) or the BASE64 encoded SHA256 hash (recommended). If it is the plain code verifier code_challenge_method is set to plain, else it is set to S256.

4) Send the code_verifier with the token request

On the access token request, the code_verifier is sent so the authorization server can verify that the client requesting the tokens are the same that did the authorization request in step 3.

If the code challenge method is plain, then the authorization server simply checks that the provided code_verifier matches the stored code challenge.

If the code challenge method is S256, then the authorization server should take the provided code_verifier and perform the hash and base64 encoding to check if it equals the store code challenge.

How do I do this in an Angular app?

To see how to set this up in an Angular app read this post.

Conclusion

In this post, we looked at the differences between implicit flow and code flow with PKCE which is now the recommended way of doing authentication in public clients. We looked at the security vulnerabilities of implicit flow and how code flow with PKCE overcomes these. Lastly, we looked at how code flow with PKCE works and what makes it different from regular code flow.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »

How to Set Up Git Hooks in an Nx Repo

Git hooks can be used to automate tasks in your development workflow. The earlier a bug is discovered, the cheaper it is to fix (and the less impact it has). Therefore it can be helpful to run tasks such as linting, formatting, and tests when you are e.g. committing and pushing your code, so any

Read More »

The Stages of an Angular Architecture with Nx

Long gone are the times when the frontend was just a dumb static website. Frontend apps have gotten increasingly complex since the rise of single-page application frameworks like Angular. It comes with the price of increased complexity and the ever-changing frontend landscape requires you to have an architecture that allows you to scale and adapt

Read More »

The Best Way to Use Signals in Angular Apps

Since Angular 16, Angular now has experimental support for signals and there is a lot of confusion in the community about whether this is going to replace RxJS or how it should be used in an app in combination with RxJS. This blog post sheds some light on what I think is the best way

Read More »

High ROI Testing with Cypress Component Testing

Testing is one of the most struggled topics in Angular development and many developers are either giving up testing altogether or applying inefficient testing practices consuming all their precious time while giving few results in return. This blog post will change all this as we will cover how I overcame these struggles the hard way

Read More »