The Basics of OAuth 2.0: Authorization code flow, Implicit flow, state and PKCE

Alysa Chan
12 min readJan 16, 2022

--

As a beginner learning authentication in back-end development, I come across the topic of OAuth 2.0. In this article, I try to summarize my learning on:

  • OAuth 2.0 concept
  • Authorization code flow and implicit flow with Google OAuth 2.0 API example
  • Common CSRF attack, state parameter and PKCE

Here is another beginner-friendly article about the topics I cover in this article.

Core concept of OAuth 2.0

OAuth is about the process of a user authorizing an application to have a limited access to his data in a third-party application, like Google.

Here is an OAuth 2.0 example that you may have already experienced:

Image from Google Open ID Connect documentation

This situation happens when you are visiting a website, which requires your data from Google. The website needs your permission to get your Google Drive data from Google. It exactly shows the core concept of OAuth, suggested in the official RFC6749:

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.

Back to our Google example, with the OAuth concept, we can see few parties involved in this process:

  • Resource owner (you)
  • Client (the website you are visiting)
  • Resource server / Authorization Server (Google)

Details about the roles are mentioned in RFC6749

Google Drive example

Here is a simple example of these 3 parties.

  1. You are Tom, who visits a website that needs to get access to your Google Drive data
  2. The website requests Google to get your Google Drive’s data
  3. Google asks for your consent
  4. If you agree to grant permission for the website to get access to your Google Drive’s data, Google returns your data to the website

Concept 1: OAuth is about authorization, not authentication

Some people may relate authentication with OAuth. However, OAuth is about authorization, which stands for open authorization. It is not originally designed for authentication.

If we have to implement authentication with OAuth, we can use open ID Connect , an industry standard built on top of OAuth 2.0, for authenticating users. This topic is not covered in this article.

Concept 2: Website won’t obtain your confidential information

The advantage of using OAuth 2.0 is to protect user’s confidential information in the third party service. In our previous Google Drive example, the website won’t obtain the user’s confidential information in his Google account, such as his Google password.

Grant types

Here are some common grant types in OAuth 2.0:

  • Authorization code flow
  • Implicit flow
  • Client credentials flow (It will not be covered in this article)

Imagine your website has to display the user’s Google Drive data. There are two grant types in Google OAuth 2.0 API which you can choose:

  1. Authorization code flow (for server-side web app)
  2. Implicit flow (for client-side web app)

Prerequisites: register your app

Before we start, no matter which grant types we choose to implement, we have to register our web applications in that third-party service. It will return the credentials we need for the next step, such as client ID, client secret etc. Details can be found in third-party service, such as Google OAuth 2.0 API documentation.

After we have register our apps, we can start implementing the OAuth 2.0 framework.

Authorization code flow

When do you use authorization code flow?

You are able to store client secret in your server to avoid exposing it to the public. If you can only work for the front-end side, such as single-page applications (SPA) or mobile apps, which will leak the client secret to the public, it is strongly recommended to use authorization code with PKCE, which will be discussed later in this article.

Steps

To focus on implementing authorization with OAuth, I simplify the explanation by skipping the login process, i.e authentication, here. Imagine we have to display user’s Google Drive data once he gets into our website, the process works as described below:

In short, after user agrees to grant permission, the website’s server:

  1. Receives an authorization code in the callback URL
  2. Exchanges the authorization code for access token and refresh token
  3. Calls Google Drive API to get user’s Google Drive data with the access token (client id and client secret are also required)

Authorization code

  • Short-lived and for single use
  • If authorization server receives multiple attempts to exchange authorization code for access token, the server should revoke all access tokens that have been granted already
  • Token for exchanging access token and refresh token. After the user agrees to grant permission, the authorization server (e.g Google’s server) will direct the user to a URL which includes the authorization code as query:
https://oauth2.example.com/auth?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7

Then the website’s server can exchange it for access token and refresh token, from the authorization server (e.g Google’s server)

Access token

  • Token required for getting user’s Google resource
  • Short-lived and periodically expires

Refresh token

  • Token for getting a new access token, when the previous access token expires. User’s permission is not required during this process

For Google OAuth 2.0 API:

  • In Google OAuth API, normally refresh token never expires, unless it is revoked. However, as mentioned in Google’s documentation, refresh tokens for Google Cloud platform project with status ‘testing’ works differently:

A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of “Testing” is issued a refresh token expiring in 7 days.

  • Also, in Google OAuth API, there’s a limit for issuing refresh token for Google account for per OAuth 2.0 client ID. It is recommended to store the refresh token permanently.

But why can’t the server directly return access token and refresh token?

You may wonder why cannot the server return access token and refresh token directly? What’s the point of using an authorization code? Like this:

Indeed, the idea suggested above does workable. This flow is called implicit flow, which is another grant type. The third-party directly returns access token and refresh token, without issuing an authorization token. However, this flow is insecure because access token and refresh token may be exposed in the frond-end side, which can be stolen easily.

Implicit flow mentioned in RFC6749:

The implicit grant is a simplified authorization code flow optimized for clients implemented in a browser using a scripting language such as JavaScript. In the implicit flow, instead of issuing the client an authorization code, the client is issued an access token directly

When the user agrees to grant permission, the third-party service (e.g Google) will make a GET request with the callback URL we have set in Google console before. Like this:

http://localhost/callback?code={authorization code here}

Imagine the server directly returns access token and refresh token in the URL, like this:

http://localhost/callback?access_token={...}&refresh_token={...}

The attacker can steal the tokens here and directly call Google Drive API to obtain the user’s Google Drive data. For example, now the tokens are exposed to all JavaScript in the user’s browser, including JavaScript from third-party libraries. Any common XSS attacks can easily steal the tokens here.

Although implicit flow is relatively insure compared to authorization code flow, Google does support implicit flow for JavaScript Web Apps. In this case, Google suggests developer to use Google’s JavaScript client library for the implementation for to have better security. Details can be founded here.

Implicit flow

As mentioned above, implicit flow is another grant type. It is a ‘simplified’ version of authorization code flow, which skips the authorization code part and directly returns an access token. If the hacker steals the access token, he can call Google API, such as Google Drive API, to get the user’s Google Drive data.

Take Google OAuth API as an example, Google provides implicit flow for client-side web applications. There are 2 approaches to implement the flow:

  • Use gapi.auth2 and gapi.client objects, provided by Google JavaScript API Library to handle user's authorization and authorization request (Officially recommended approach)
  • Directly call OAuth 2.0 endpoint without using any library

No matter you use JavaScript API Library or directly using OAuth 2.0 endpoint approach, the access token will be exposed in the front-channel, which may be stolen in XSS attack.

Does it mean that authorization code flow is secure enough?

Compare to implicit flow, authorization code flow is more secure since we exchange tokens in back-channel. In the example above, the website’s server exchange tokens with Google’s server. This process happens in back-channel and tokens are not exposed to the front-end side.

However, that’s not secure enough when it comes to preventing cross site request forgery (CSRF) attack.

Common CSRF attack

The attack can cheat the user to send an unintentional request. Consider the example below:

In the example above, given that:

  • My website has a feature that create files in user’s Google Drive
  • After user signs in my website, a JWT token is stored in the user’s cookie. User can connect to his Google Drive after signs in my website, by using OAuth 2.0 flow

The CSRF attack starts with the following steps:

  1. Hacker signs in my website and connects his Google Drive
  2. Hacker gets the redirect URL which contains authorization code returned by Google
  3. Hacker stops running the OAuth 2.0 here
  4. Hacker develops a malicious website / link. Later a user, Tom visits it. Tom is a user of my website and has signed in already before
  5. He clicks on that malicious website / link, it triggers a GET request to my website. (The “cross-site” part in CSRF), with Tom’s JWT in his browser’s cookie
  6. My server validates Tom’s cookie and sends a request to Google, for exchanging access token and refresh token
  7. Now Tom’s account in my website is connected to hacker’s Google Drive. When Tom visits my website and use my website’s feature to create file in Google Drive, he create files in hacker’s Google Drive

But can CORS setting block cross-site attack?

I thought of this question when I was learning CSRF:

If my website has only allow domain https://myexamplesite.com to access my resource by setting CORS, wouldn't my website block the GET request in step 7 in the diagram? Because the hacker's website, e.g https://evilhackersite.com) does not have the same origin as my website (e.g https://myexmaplesite.com).

However, as mentioned in this discussion, we can’t prevent CSRF attack by setting CORS:

… This means that if you are performing a CSRF attack on a vulnerable site which results in some server side state change (e.g. user creation, document deletion etc), the attack will be successful but you would not be able to read the response.

With CORS setting, the attacker can still send a request to my server and my server will react to it. The limitation is that he cannot read the responses of his request.

Solution: Request authorization code with ‘state’

The cause of the security issue above is that we cannot verify whether the authorization code we are using is requested by the user or the hacker. To solve the problem, we can add state parameter, which is an anti-forgery state token, for verification.

For example, Google recommends developer to generate and add state parameter in the URL for requesting the authorization code, at the beginning of the OAuth 2.0 flow:

Screenshot from Google OAuth server-side Web Apps documentation

Steps

  1. At the beginning of the OAuth 2.0 flow, my server requests authorization from Google, by generating a URL, the process here is same as the authorization code flow mentioned before.
  2. My server creates token, which is an anti-forgery state token and store it in the server. Also, add a state parameter in the URL we have generated. Here's an example mentioned in Google doc:
https://accounts.google.com/o/oauth2/v2/auth?
scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&
access_type=offline&
include_granted_scopes=true&
response_type=code&
state=state_parameter_passthrough_value&
redirect_uri=https%3A//oauth2.example.com/code&
client_id=client_id

The token can be a random string with 30 or so characters. Then my server sends the request to Google’s server.

Finally, Google returns a callback URL with authorization code and state parameter. My server checks if the state token in the URL is same as the state token stored in my server. If yes, we can confirm that the authorization code is really requested by the user and can be used to exchange for an access token and refresh token.

Authorization Code Interception Attack

Another security risk is that the authorization code may be intercepted by the hacker. Then he can get the access token and the user’s data.

Image from RFC7636

As suggested in RFC7636, the security issue happens if:

  • A malicious app is registered in the client’s device
  • The malicious app registers the same custom URI scheme as the legitimate OAuth 2.0 App’s custom URL, which can handle the response which contains an authorization code
  • The hacker knows the client secret. For example, SPA or mobile app will expose client secret to the public

In step 2 and 3, the communication is protected by TLS and cannot be intercepted by the hacker.

Solution: PKCE (Proof Key for Code Exchange)

From the issue above, we now know that authorization code is not reliable. To solve the issue, we use PKCE.

Official definition of PKCE:

PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks.

In short, my application can dynamically create 3 things when requesting for an authorization code:

  • code_verifier
  • code_challenge
  • code_challenge_method

Image from RFC7636

  • t(code_verifier) = code_challenge
  • t_m = code_challenge_method

Although PKCE is originally designed for improving authorization code flow, PKCE is can also be used in implicit grant type.

Steps

  1. My app generates a code_verifier, high-entropy cryptographic random string.
  2. My app generates a code_challenge, by using one of the following transformations on the code_verifier:
    - plain (strongly discouraged!)
    code_challenge = code_verifer
    - S256
    code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
  3. My app generates a code_challenge_method, which is S256 here.
  4. My app sends a request with these 3 values to the authorization server. The server returns an authorization code.
  5. My app requests an access token from the server, with:
    - Authorization code
    -code_verifier
  6. The authorization server transforms code_verifier with the code_challenge_method I sent to it in step 4. It compares the result with the code_challenge it received before in step 4.

As a result, if the hacker intercepts the authorization code, he fails to get access token since he cannot intercept the code_verifier I dynamically created in the application.

Google OAuth 2.0 API with PKCE

In Google OAuth 2.0 API, Google supports PKCE in Mobile & Desktop Apps. However, at the time I am writing this article, it seems that Google OAuth 2.0 API does not support PKCE in JavaScript web apps, such as SPA, even though implicit flow with PKCE is a recommended practice for SPA. The stackoverflow discussion here addresses the same idea.

PKCE VS state?

Now we understand the OAuth 2.0 security issues that handled by state parameter and PKCE. Be aware that PKCE and state solves different problems, that means they cannot replace each other.

  • PKCE: Prevent authorization interception
    If my authorization code is stolen by the hacker, he still cannot exchange it for an access token since he cannot steal the code_verifier my app created dynamically
  • state parameter: Prevent to use hacker's authorization code
    To ensure the authorization code is really requested by the user, not the hacker, my server creates a random string as 'state'. It will be stored in my server and sent to the authorization server. When the authorization returns the callback URL with authorization code to my server, it has to also contain a state parameter with the same value I stored in my server.

In other words, we can have both PKCE and state parameter in our OAuth 2.0 implementation for better security. One example is the Google OAuth 2.0 API for mobile & desktop apps API mentioned above.

Summary

I summurize what I have learnt about OAuth 2.0 with Google OAuth 2.0 API in this article. It introduces the basics of OAuth 2.0 concept, grant types including Authorization code flow and Implicit flow, the purpose of state and PKCE. Feel free to share your opinions on this topic with me!

--

--