From the Implicit Flow to PKCE — OAuth 2.0 for SPA and Mobile Apps

How can we make the OAuth flow secure in Single Page Applications and Mobile App? In this article, let’s have a look at a legacy way to achieve this back in 2010 and see how it evolved to today’s best practices.

Image by Unsplash.

First, you should have a little background about OAuth and OIDC and understand why saying “Login by OAuth” is bad. I recommend these following resources:

Why does Implicit Flow exist?

We already have a very good working Authorization Code flow, why invent another flow? Why SPA and Mobile Apps don’t use Authorization Code flow?

Turned out that it can’t. First, SPA and Mobile Apps are public clients, there is no appropriate way to hide secrets so there is no client secret stored in these kinds of app, only public client_id. In the authorization code exchange step, Authorization Code flow require client secret to authenticate itself to Authorization Server but it can’t since public clients don’t have client secret.

The second reason is that back in 2010, the browser works differently than they do now. Javascript can only make requests to the same server that the page was loaded from (same-origin policy). This limitation makes the code exchange request impossible for SPA because Authorization Server is usually in a different domain vs deployed Javascript code domain.

POST request by Javascript in step 6 is not allowed due to the same-origin policy.

For the above reasons, the authorization code exchange step can’t work in SPA and Mobile Apps, so instead the shortcut was to return the access token in the authorization response in the browser (step 5). That’s how Implicit Flow works. OAuth 2.0 recommends Implicit Flow for SPA.

Why it’s bad?

Implicit Flow looks super simple, we cut the authorization code exchange step but what’s the catch?

If you read in the resource I gave you before, just a recap, return access token in the front channel is bad because the front channel is not a secure environment, user can be tricked to install a malicious browser extension, which can listen to user’s network or access to browser history or physically just simple as stand behind user’s back and snooping for the token.

Here is what OAuth 2.0 Security Best Current Practice RFC say about Implicit Flow:

The implicit grant (response type “token”) and other response types causing the authorization server to issue access tokens in the authorization response are vulnerable to access token leakage and access token replay …

Moreover, no viable mechanism exists to cryptographically bind access
tokens issued in the authorization response to a certain client as it
is recommended in
Section 2.2. This makes replay detection for such
access tokens at resource servers impossible.

Also, no refresh token is issued during the implicit flow. Therefore:

  • Long live access tokens are often needed
  • When the client needs additional/new token, either do a silent redirect (prompt=none) with a hidden iframe or have to send the user to an ID provider and re-authenticate the user.

Although having all these risks, Implicit Flow was the only way possible at that time. We could mitigate those risks by limiting the privileges and using short live token (with silent redirect for good UX). That is the usual way people do at that time. (However, I should say that now, SameSite=Lax makes silent redirect with a hidden iframe impossible)

Nowadays, Cross-Origin Resource Sharing (CORS) is widely adopted by browsers. CORS provides a way for JavaScript to make requests to servers in different domains, as long as the destination (IdP) allows it. This opens up the possibility of using Authorization Code flow in JavaScript.

Authorization Code Flow

Let’s see what happened if we use only Authorization Code Flow for public client SPA. Here we have a sequence diagram:

Access Token hijack on Authorization Code Flow for Public Client

Assuming the user’s network is being monitored and attackers can listen to all user’s requests. In step 4 after the user login to Authorization Server, Authorization Code is returned. Due to being able to listen to the user’s network activity, attackers can steal the authorization code and the idea is to exchange it before the real user does it. This actually works because, for public clients, Authorization Server doesn’t request client secrets to exchange for access token. Other information like client id, redirect_uri, scope, etc, all can be retrieved in the request to Authorization Server, attacker can get all the required information and forge a legitimate token exchange request.

That’s why for public clients, PKCE is recommended to use. Let’s see what happened if we add PKCE on top of Authorization Code Flow:

Authorization Code Flow with PKCE

PKCE: Proof Key for Code Exchange

(Also noted that in Step 12, when setting up the application, the client must register with Authorization Server the website URL where the client will make the cross-origin request from.)

In step 2, the client generates code_verifier and code_challenge, the way to create it is to choose a one-way hash method, hash a plain text and get a hashed string.

code_challenge_method(code_verifier) = code_challenge# which equivalent tohash_method(plain_text) = hash

The client keeps code_verifier and sends code_challenge_method code_challenge in the authorization request in step 3. The Authorization Server will save these 2 values.

In the token exchange step, Authorization Server requires client_verifier from the client in step 12 and validate code_challenge received in 3 by running the hash method on code_verifier, just exactly like what the client did at step 1:

code_challenge_method(code_verifier) == code_challenge?

For the fake client, it doesn’t have code_verifier so the validation will be failed!!! This way, code_vefirier acts as a temporary Client Secret!

Clients MUST prevent injection (replay) of authorization codes into the authorization response by attackers. Public clients MUST use PKCE [RFC7636] to this end. For confidential clients, the use of PKCE [RFC7636] is RECOMMENDED.

Refresh Token for Public Client

One nice thing about using Authorization Code Flow is that clients can have Refresh Token, which is a very powerful thing. If it is leaked, it can be used indefinitely. So it’s up to the policy of the Authorization Server, based on risk assessment, whether to issue a Refresh Token in step 15 above.

If refresh tokens are issued, the Authorization Server need to manage these tokens carefully against malicious actors by following methods:

  • Sender-contrained refresh tokens
  • Refresh token rotation

More detail in section OAuth 2.0 Security Best Current Practice RFC#Section 4.13.2.

Does the Authorization Code Flow make SPA totally secure?

After the user closes the app and opens it again, we don’t want the user to have to start the flow again, that’s very bad UX. So in step 17 above, after we obtain the access token, we need to store it in some way for later use.

Authorization Code Flow with PKCE makes sure the access token is safe during transit. However, the problem is how we can store this access token. This problem still exists whether we use the Implicit Flow or the new recommended flow.

So the answer is no. The problems still lie in the front channel, where it can’t store secrets. The best way (currently) is to keep the token management outside of Javascript entirely, by bringing the Authorization Code Flow to a confidential client.

Conclusion

Hopefully, you can have a general idea of why Implicit Flow was not recommended for SPA and Mobile App. In the next article, I will show you Security Best Practice for SPA and Mobile App so stay tuned for the next article. ^^

Resources

Thanks for reading this article! Leave a comment if you have any questions. If you found this article helpful, please hold the clap button so that others can find this. Be sure to sign up for my newsletter below or follow me on Medium to get more articles like this. ☝️👏 😄

Microservice | Blockchain | Fullstack