Understanding OAuth 2 with PKCE in Single-Page Applications (2020)
Tags: security, oauth2, javascript, webdev
Published: 2020-09-29 10:11:39

---
title: "Understanding OAuth 2 with PKCE in Single-Page Applications (2020)"
published: true
description: "An introduction to OAuth 2 with PKCE for single-page applications."
tags: security, oauth2, javascript, webdev
canonical_url: https://www.valentinog.com/blog/oauth2/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/li9tticop68pn4nbwcce.png
---

**Token-based authentication on the web** is one of those things you know exists, but sometimes you're too scared to implement in your  projects.

**Authentication for SPA** (single page applications) is even more **scary**, but willing or not you should know a bit of theory to work proficiently with any SDK when the time comes.

In this post I hope to clarify for you the current recommended **OAuth 2 flow** for single-page applications: **authorization code grant with PKCE**.

## Who should read this post

Frontend developers who already worked with OAuth 2 in the past, and want to learn more about what's behind **authorization code grant with PKCE**. 

Requirements: you know that OAuth 2 is a security protocol for authorization delegation, and you know what an OAuth application is.

If you need a refresh on the basics instead, check out [OAuth 2.0 Simplified by Aaron Parecki](https://www.oauth.com/).

## A word of warning

**Security on the web is a minefield**. First of all, **do not use my examples for production code**. They serve **just as a pointer for helping you understand OAuth 2. I'm not suggesting you write your own OAuth client**. Also:

- Don't roll out your own crypto.
- Don't roll out your own authorization servers by hand. Use solid libraries instead.
- Don't use [localStorage](https://www.rdegges.com/2018/please-stop-using-local-storage/) for saving authorization codes, or worst, tokens.

Enjoy!

## Terminology

Albeit boring, to start off we need to outline **OAuth 2.0 terminology**.

- **Resource owner**: (simply **user** from now on) the user who owns permissions on a third-party service. **Example**: any user who has an account on Github, or Twitter, or on a generic service exposing an API.
- **Authorization server**: also called **AS**, is the service which grants access to a **client** on a resource server **on behalf of the user**.
- **Resource server**: the **third-party service** that the user want to delegate access on. **Example**: Github, Twitter, Google Drive, or any protected API. Sometimes the resource server and the **authorization server** run on the same server.
- **Client**: a single-page application or some leaner JS frontend which needs access to the resource server. **Example**: a single-page app to display your Github repos.
- **Scope**: it answers the question "**what the client can access on the resource server**"?

For simplicity, in the following example **we condense resource server and authorization server in a single place**.

Let these terms **sink in**, and once confident head over the next section.

## The flow 

We are developing a **single-page application** that eventually will be distributed, but for now should only **access a resource on a third-party service** on which we have an account.

Here are the steps we need to take to abide to the **OAuth 2.0 flow** for single-page applications:

0: **User** registers and logins on the third-party service, creates a new OAuth application. **Users** obtains `client_id`.
1: **User** visits the **client**, and clicks a link which **takes** it to the **authorization server**.
2: The **authorization server** asks the **user** which permissions (**scope**) should delegate to the **client**.
3: Once the **user** accepts, the **authorization server** redirects the **user** back to the **client**, with an authorized code attached in the URL.
4: **Client** uses the authorization code to request an **access token** from the authorization server.
5: **Client** sends back the token on each subsequent request to a protected resource on the resource server, until the access token expires.
6: Eventually, **client** uses a refresh token to request a new access token.

Not covered in this post: scopes, the logout phase, handling of the refresh token.

Let's now see the flow in more detail.

### 0: User registers and logins to the service

As a prerequisite for the OAuth2.0 flow, the **user** has to have a **registered account** on the authorization server. She must also create an **OAuth application** on the authorization server.

Suppose **users** want to **delegate** access to Github to a single-page application she's building. She first registers with Github, maybe she creates a couple of repos, and then she creates a new **OAuth application** in Settings -> Developer settings -> OAuth apps.

If the user/developer controls also an API, say a Django REST API, with a package like [django-auth-toolkit](https://github.com/jazzband/django-oauth-toolkit) she can configure an OAuth 2.0 provider in a few minutes. Here's for example the admin interface for adding a new OAuth application in Django:

![django-auth-toolkit add application](https://dev-to-uploads.s3.amazonaws.com/i/0l8vp1ootvzmeqc2hffr.png)

In this phase she also provides one or more `redirect_uri`, where the authorization server will redirect the user. Once done, the service gives back a `client_id` code, and optionally a `client_secret`.

Single-page applications can't safely store a `client_secret`, so they rely only on `client_id` and **PKCE** (more on this later).

**Next step**: user visits the client, and clicks a link which takes it to the authorization server.

### 1: User -> Client -> Authorization server

This is the one most important part of the flow.

In this part of the flow, the **user** visits the **client**, normally a webpage or a single-page application. There she finds a link to follow, which takes her to the **authorization server**.

Alongside with the request, the **client** should pass a bunch of **query parameters** in the URL. Those are at least:
 
- `client_id`: the id obtained during the OAuth application registration.
- `response_type`: the grant type. In our case it's **authorization code grant type** so we use `response_type=code`.
- `state`: a random string used for CSRF protection.
- `code_challenge`: part of PKCE, **Proof Key for Code Exchange**. More on this later.
- `code_challenge_method`: part of PKCE, the hashing algorithm.
- `redirect_uri`

We could also use a `scope` parameter to narrow down the permissions for the client, not used in this example.

Now assuming:
 
- `https://client.example/` is the client
- `https://client.example/auth` is the client redirect URI
- `https://auth.example/oauth/authorize` is the authorization server

The developer can craft the following link in the page:

```text
https://auth.example/oauth/authorize
?client_id=ERPn7zeLvPCnZ1OtF7jxlTiZz0uQjpiWQEdeAUva
&response_type=code
&state=nMdR7oTxgXYr0INLytRi9XH7InnbrNbg
&code_challenge=M5oPBP6RmJPh5QsGtx6ptVH7S2yjqt8sum96jBCyhZg
&code_challenge_method=S256
&redirect_uri=https://client.example/auth
```

(Note: the client id here has nothing to do with the previous image).

`state` is a random string, you can generate it with any JavaScript library:

```javascript
import randomstring from "randomstring";

const state = randomstring.generate();
```

For Node.js there is [randomstring](https://www.npmjs.com/package/randomstring), but on the frontend you need to find something lighter or tree-shakeable.

Next up, where do we get `code_challenge`? To generate `code_challenge` we:

1. Generate first a `code_verifier`. This should be a high entropy string. The longer the string, the better (maximum 128 characters as per spec): 

```javascript
import randomstring from "randomstring";

const state = randomstring.generate();
const code_verifier = randomstring.generate(128);
```

It's important to remember that `code_verifier` and `state` need to **persist somewhere in the browser** because we need them in the next steps. Persisting these two values is **harmless**.

You can put them in `sessionStorage`:

```javascript
import randomstring from "randomstring";

const state = randomstring.generate();
const code_verifier = randomstring.generate(128);

function saveStateAndVerifier() {
  /*
  Don't overwrite our saved state if location has the state parameter.
  This means we got authorization from the AS, and we need to compare them later.
 */
  if (window.location.search.includes("state")) return;
  const storage = window.sessionStorage;
  storage.clear();
  storage.setItem("state", state);
  storage.setItem("code_verifier", code_verifier);
}

saveStateAndVerifier();
```

2. Then we compute `code_challenge` starting from `code_verifier`:

```javascript
import randomstring from "randomstring";
import { encode as base64encode } from "base64-arraybuffer";

const state = randomstring.generate();
const code_verifier = randomstring.generate(128);

function saveStateAndVerifier() {
  /*
  Don't overwrite our saved state if location has the state parameter.
  This means we got authorization from the AS, and we need to compare them later.
 */
  if (window.location.search.includes("state")) return;
  const storage = window.sessionStorage;
  storage.clear();
  storage.setItem("state", state);
  storage.setItem("code_verifier", code_verifier);
}

saveStateAndVerifier();

async function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await window.crypto.subtle.digest("SHA-256", data);
  const toBase64 = base64encode(digest);
  // you can extract this replacing code to a function
  return base64Digest
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

generateCodeChallenge(code_verifier).then((challenge) => {
  console.log(challenge);
  // whatever you generated, example:
  // M5oPBP6RmJPh5QsGtx6ptVH7S2yjqt8sum96jBCyhZg
});
```

This is probably the most difficult part. For a complete explanation see ["Generating the code challenge for PKCE in OAuth 2"](https://www.valentinog.com/blog/challenge/).

What matters here is that you're left with a `code_challenge`, a string like `M5oPBP6RmJPh5QsGtx6ptVH7S2yjqt8sum96jBCyhZg`. This will be sent later to the authorization server.

Once you have everything you can [build the URL](https://www.valentinog.com/blog/url/):

```javascript
// These should go in environment variables injected at build time
// I put it here to keep things simple
const client_id = "ERPn7zeLvPCnZ1OtF7jxlTiZz0uQjpiWQEdeAUva"
const redirect_uri = "https://client.example/auth";

// omitted the rest for brevity

generateCodeChallenge(code_verifier).then((challenge) => {
  buildLoginUrl(challenge);
});

// Proto-code. Don't use for production
function buildLoginUrl(challenge) {
  const link = document.querySelector("a");
  const linkValue = new URL("https://auth.example/oauth/authorize");

  const queryParams = {
    client_id, 
    response_type: "code",
    state: state,
    code_challenge: challenge,
    code_challenge_method: "S256",
    redirect_uri
  };

  for (const param in queryParams) {
    linkValue.searchParams.append(param, queryParams[param]);
  }

  link.setAttribute("href", linkValue);
}
```
 
At the end it should translate to something along these lines:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>OAuth 2.0 in Single-Page Applications for Mere Mortals</title>
</head>
<body>
<a href="https://auth.example/oauth/authorize
?client_id=ERPn7zeLvPCnZ1OtF7jxlTiZz0uQjpiWQEdeAUva
&response_type=code
&state=nMdR7oTxgXYr0INLytRi9XH7InnbrNbg
&code_challenge=M5oPBP6RmJPh5QsGtx6ptVH7S2yjqt8sum96jBCyhZg
&code_challenge_method=S256
&redirect_uri=https://client.example/auth">LOGIN</a>
</body>
</html>
```

You can see an example of this first step any time you're about to log in on a service with "Login with GitHub" or "Login with Google". In most frameworks this is called social authentication.

![Continue with Github](https://dev-to-uploads.s3.amazonaws.com/i/10jxnc1biyzlwsrewwln.png)

**Who codes this part**? This part of the flow is a responsibility for the frontend developer. In most cases, it is recommended to use the SDK provided by your authentication provider.

**Next step**: authorization server asks the user for permissions.

### 2. Authorization server asks the user for permissions

As a prerequisite for this step, the **user** must be logged in the third-party service. If that's the case, the user is presented with a screen which asks for **permissions** on the **resource server**:

![Authorization server asks the user for permissions](https://dev-to-uploads.s3.amazonaws.com/i/k1609mp4kw6p8b7sgfhy.png)

This is again an example with django-oauth-toolkit.

Remember: here **we condense resource server and authorization server in a single place**, but in the real world this is not always the case.

If the user gives consent, the **authorization server** redirects the user to the `redirect_uri` specified in step 0. 

**Who codes this part**? This part of the flow is a responsibility for the backend developer who should prepare an OAuth 2.0 capable server using proven and tested packages.

**Next step**: authorization server redirects the user back to the client.

### 3. Authorization server -> Client -> User

Once the **user** authorizes the **client**, the authorization server redirects the **user** to the `redirect_uri` provided in step 0 during the OAuth application registration, in our example `https://client.example/auth`.

The redirect URL will carry in addition an **authorization code** as a query parameter, as well as `state`:

```text
https://client.example/auth
?code=l2Z1DNMaiJWZEa3iZJsChdUeKraG3k
&state=nMdR7oTxgXYr0INLytRi9XH7InnbrNbg
```

A couple of important points:

- Don't save the **authorization code** in `localStorage`, there's no need.
- You can notice how `state` from this request is the same `state` we computed in step 1.

In this step we need to compare the `state` we get from the authorization server, and the `state` we saved in `sessionStorage` in step 1.

If they are not the same, we must alt the flow:

```javascript
const currentLocation = new URL(window.location.href);
const stateFromLocation = currentLocation.searchParams.get("state");

if (window.sessionStorage.getItem("state") !== stateFromLocation){
    throw Error("Probable session hijacking attack!");
}
```

**Who codes this part**? This part of the flow is a responsibility for the frontend developer who ensures that the redirect URI is accessible.

**Next step**: client uses the authorization code to request an access token from the authorization server.

### 4. Client uses the authorization code to request access token

At this point the JavaScript client has everything in place for **requesting an access token**. 

This request should reach the authorization server at `https://auth.example/oauth/token` by the means of a `application/x-www-form-urlencoded` `POST` request.

There are a lot of things that need to go in the request body. Namely:

- `client_id`: we have this from the beginning.
- `grant_type`: this should be `authorization_code`.
- `state`: we saved this in `sessionStorage`, step 1.
- `code`: we get this from the current location.
- `code_verifier`: : we saved this in `sessionStorage`, step 1.
- `redirect_uri`: we have this from the beginning.

To get the access token, the developer crafts a function which runs right after the redirect flow. The following code is a loose idea of how it should look like:

```javascript
// Proto-code. Don't use for production
function getToken() {
  const tokenEndpoint = new URL(
    "https://auth.example/oauth/token"
  );

  const currentLocation = new URL(window.location.href);
  const authorizationCode = currentLocation.searchParams.get("code");
  const stateFromLocation = currentLocation.searchParams.get("state");
  const initialCodeVerifier = window.sessionStorage.getItem("code_verifier");

  // This is a good place for checking the state too  
  if (window.sessionStorage.getItem("state") !== stateFromLocation){
    throw Error("Probable session hijacking attack!");
  }

  const queryParams = {
    client_id,
    grant_type: "authorization_code",
    state: stateFromLocation,
    code: authorizationCode,
    code_verifier: initialCodeVerifier,
    redirect_uri
  };

  for (const param in queryParams) {
    tokenEndpoint.searchParams.append(param, queryParams[param]);
  }

  fetch(`${tokenEndpoint.origin}${tokenEndpoint.pathname}`, {
    method: "POST",
    body: tokenEndpoint.searchParams,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Accept: "application/json"
    }
  }).then(/* more later */)
}
```

Here we get `state` and `code` from the current location, we check that the previous `state` and the `state` from the authorization server match.

Next up, we build the query parameters, and we send the `POST` request. The **authorization server must allow CORS** for this to work.

If everything goes well, the **authorization server responds with a Bearer access token**, and a refresh token. Here's an example of response:

![Oauth 2.0 Bearer access token](https://dev-to-uploads.s3.amazonaws.com/i/u7vc1284t1iy076upohl.png)

**Who codes this part**? The frontend developer puts together the logic for building the `POST` request. In most cases, it is recommended to use the SDK provided by your authentication provider. The backend developer ensures that the authorization server exposes the appropriate **CORS headers**.

**Next step**: Client uses the access token to access a protected resource.

### 5. Client sends the token to access a protected resource

This is probably the "easiest" part. In the `getToken()` function we add a minimal logic for saving the JSON response, which now holds access and refresh token, as well as the expiration. Here's only the relevant snippet:

```javascript
let tokens = null;

function getToken() {
// omit

  fetch(`${tokenEndpoint.origin}${tokenEndpoint.pathname}`, {
    method: "POST",
    body: tokenEndpoint.searchParams,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Accept: "application/json"
    }
  })
    .then(response => {
      if (!response.ok) throw Error(response.statusText);
      return response.json();
    })
    .then(json => {
      // save the tokens
      tokens = json;
    });
}
```

In $insertYourFrameworkHere you will save these tokens in the ephemeral state of the application.

Next up, we **send the access token on each subsequent request to the resource server** (a REST API for example), until the token expires. 

To do so, we pass an `Authorization` header with the bearer token. For example, we might want to fetch a list of resources on a button click:

```javascript
const button = document.querySelector("button");
button.addEventListener("click", fetchData);

function fetchData() {
  fetch("https://auth.example/api/customers/", {
    headers: {
      Authorization: `Bearer ${tokens.access_token}`
    }
  }).then(/* do stuff with the response */);
}
```

We imagined from the beginning that in our example the **resource server**, and the OAuth 2 **authorization server** live on the same machine. Hence, we call the API at `https://auth.example/api/customers/`.

**Who codes this part**? The frontend developer puts together the logic for handling the access token. The backend developer ensures that the REST API (resource server in our case) exposes the appropriate **CORS headers**.

## What's next?

At this point we have a minimal working implementation of a rudimentary Auth 2 client. But we lack **logout, scope handling, and the logic for using the refresh token**.
 
Authorization providers like AWS or Okta have already solved this problem for us, and that's what most companies use in the real world.

If anything, hopefully you learned something new, and the theory behind **authorization code grant with PKCE**.

## Wrapping up 

OAuth 2 is hard, there are **a lot of ways you can shoot yourself in the foot**. In the frontend, always program defensively. In particular:

- Use the **OAuth SDK** from your authorization provider.
- Don't roll out your own crypto.
- Don't use [localStorage](https://www.rdegges.com/2018/please-stop-using-local-storage/) for saving the authorization code, even if it's temporary.
- Don't save the access token, or the refresh token in `localStorage`!
- Again, use the **OAuth SDK** from your authorization provider.
- Again, don't roll out your own crypto.

On the backend, **don't venture in reinventing the wheel**. There are solid, battle-tested libraries for building OAuth 2 authorization servers.

Have fun!

## Further resources

- [OAuth 2.0 Simplified by Aaron Parecki](https://www.oauth.com/) covers ins and outs of OAuth 2.0 in clear, simple steps.

My book **Decoupled Django**, (Apress, May 2021) covers OAuth 2 for Django APIs with Single-page applications. Stay tuned on my [Telegram channel](https://t.me/valentinoga) for previews and excerpts from the book!