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.
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.