Building a zero dependency PKCE Auth client
Tags: auth, pkce
Published: 2022-01-01 08:07:27

This post goes through how to build a PKCE client for browser using TypeScript based applications with no dependencies. If you want to know more about what PKCE (Proof Key Code Exchange) is you can read my [previous post, What Is PKCE](https://dev.to/kleeut/what-is-pkce-en7).  
Before I start on how do to this, as a general rule I would use a client side library provided by a reputable authentication vendor, like the [React SDK](https://github.com/auth0/auth0-react) provided by [Auth0](https://a0.to/signup-for-auth0) for any production application I was building. If you're interested in what is going on under the hood in libraries like that, or you have a specific use case, or you're just interested in a hands on example of how PKCE works then I'll go through how I implemented this only using what is available in the browser.

> The example code for this blog can be found [on GitHub](https://github.com/KleeUT/2022_0_dep_pkce_client)

A quick qualification on what I mean when I say **zero**. This is written in TypeScript so clearly there are some dependencies at play. There are no production dependencies and four dev dependencies,

1. TypeScript: for adding the wonderful development experience that is types in my code.
2. Prettier: because formatting should be an afterthought.
3. ESlint: to help catch stupid mistakes.
4. Parcel: To bundle the scripts together and serve the sample site.

## Steps

PKCE can be broken down into a number of steps. I'll address each of those in turn.

### Generate code verifier

The first step is to generate the code verifier that can be used to generate the code challenge and later used to verify that this app was in fact the app that originally requested authentication.

The code verifier is random data presented as a Base64 URL encoded string.

#### Generate the random data

The browser has some APIs that allow us to generate some random data. For this we need to use the web crypto API available at `window.crypto` or `globalthis.crypto`. The function to generate random values is `getRandomValues` this takes an `ArrayBuffer` and fills it with random data. For PKCE we need that `ArrayBuffer` to be a string. We can get this using the `String.fromCharCode` function. The random code generation function looks like this:

```ts
function randomCode(): string {
  let array = new Uint8Array(32);
  array = globalThis.crypto.getRandomValues(array);
  return String.fromCharCode.apply(null, Array.from(array));
}
```

#### Base64 url encoding the data

The final step in generating a code verifier is to base64 url encode the random string. To turn our string into base64 we can use the `btoa` function available in the browsers global scope. To make this base64 string base64 url encoded we need to replace all the `"+"`, `"/"` and `"="` with `"-"`, `"_"` and `""` (empty string) respectively, we can replace these values using a chain of `String.replace` functions and regular expressions.

The code to base64 url encode a string looks like this:

```ts
function base64URLEncode(str: string): string {
  const b64 = btoa(str);
  const encoded = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
  return encoded;
}
```

### Generate the challenge

The second step in PKCE is to generate the code challenge. This is derived from the code verifier generated in the previous step. To derive it we need to apply the SHA256 hash function to the code verifier string.

#### Hashing the code verifier with SHA256

To hash the code verifier string we can use the web crypto api provided to us by the browser. The digest function on `crypto.subtle` will create a hash for us, but we need to pass in the algorithm we want to use and the data as something that implements the [`BufferSource` interface](https://developer.mozilla.org/en-US/docs/Web/API/BufferSource).

For the first argument we want a SHA256 algorithm so we pass in `{ name : "SHA-256" }`. For the second argument we need to change our code verifier string into a `TypedArray` we can do this using an instance of the `TextEncoder` class `new TextEncoder().encode(s)`.

```ts
const sha256Hash = await crypto.subtle.digest(
  { name: "SHA-256" },
  new TextEncoder().encode(str)
);
```

Finally we need to base64 url encode our hash which has been created as an `ArrayBuffer`. To do this we need to jump through a couple of hoops to convert the `ArrayBuffer` containing our hash to a string that we can base64 using the same mechanism as previously.
Let's start by taking our `ArrayBuffer` and making it into a `TypedArray` specifically a `Uint8Array`: `new Uint8Array(hash)`. From here we can convert that into a `number` array: `Array.from(uint8Array)`. This can be converted into a string using the `String.fromCharCode` function `String.fromCharCode.apply(null, Array.from(numberArray))`. This string can be passed through the same base64 and url encoding functions as were used in the code verifier creation. The final sha256 function looks like this:

```ts
const sha256 = async (str: string): Promise<string> => {
  const hashArray = await getCryptoSubtle().digest(
    { name: "SHA-256" },
    new TextEncoder().encode(str)
  );
  const uIntArray = new Uint8Array(hashArray);
  const numberArray = Array.from(uIntArray);
  const hashString = String.fromCharCode.apply(null, numberArray);
  return base64URLEncode(base64(hashString));
};
```

#### Store the verifier

We'll need the code verifier after redirecting off to the authentication server so it needs to be stored in between renders. To do this I'll store it in the local storage. The store verifier function looks like this:

```ts
export function storeVerifier(verifier: string) {
  localstorage.setItem("verifier", JSON.stringify({ verifier }));
}
```

#### Generate the login url

The next to do is to generate the url tha we need to redirect the user to so that they can authenticate.

For this example I'm using [Auth0](https://a0.to/signup-for-auth0) as the authentication server. Some configuration will need to be done in the authentication server and some of the parameters that we need to include in the url will need to be sourced from the Auth0 dashboard.

We'll be directing the user to login using a url so any information that we need to pass to the authentication server must be passed as a query parameter to do that I'll use the `URLSearchParams` object to make things easier.

The values I'll be appending as query parameters are:

| parameter               | variable in example   | notes                                                                                                                                                                           |
| ----------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id`             | `auth0ClientId`       | The ID of the Auth0 application / client the user is logging into. Copy from Auth0                                                                                              |
| `audience`              | `auth0ApiAudience`    | The ID of the API that the token will be issued to access. Copy from Auth0                                                                                                      |
| `response_type`         | Hard coded `"code"`   | This has to be code in order to use PKCE.                                                                                                                                       |
| `scope`                 | `scope`               | The scopes (space delimited) that are being requested. These need to be configured in Auth0 or they'll be ignored                                                               |
| `recirect_uri`          | `authCallbackUrl`     | This is the url that will be redirected back to. This must be explicitly configured to be allowed in Auth0. In this case it's the same URL as the web app is being served from. |
| `code_challenge`        | `challenge`           | The challenge generated previously                                                                                                                                              |
| `code_challenge_method` | Hard coded `"sha256"` | Auth0 only supports SHA 256 as the challenge method                                                                                                                             |

These values are then appended as a query string to the `auth0BaseUrl` that is the url for the Auth0 tenant (e.g. `https://tenantname.au.auth0.com`).

```ts
function authLoginUrl(): string {
  const params = new URLSearchParams();
  params.append("client_id", auth0ClientId);
  params.append("audience", auth0ApiAudience);
  params.append("response_type", "code");
  params.append("scope", scope);
  params.append("redirect_uri", authCallbackUrl);
  params.append("code_challenge", challenge);
  params.append("code_challenge_method", "S256");
  const url = new URL(`${auth0BaseUrl}/authorize?${params.toString()}`);
  return url.toString();
}
```

Now when the user clicks on or is redirected to the URL generated by the `authLoginUrl` they will be directed to Auth0 as the authentication server. They will the be redirected back to the `redirect_url`. The next thing to do is to handle that redirect and get an access token.

### Handling the redirect

Once the user had completed authenticating at the authentication server then they're redirected back to our app and we have to pick up the code flow in order to get an identity token and access token.

#### Get the code

The first stage once the user has been redirected back to the client is to get the code. The code is passed back as a query parameter in the url. I'll do this by passing the query string to the `URLSearchParams` object constructor and checking for the presence of a `code` parameter. If the code object is there we can exchange it for access and identity tokens. The code to get the code looks like this:

```ts
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has("code")) {
  const code = queryParams.get("code");
  handleAuthorizationCodeExchange(code);
}
```

#### Retrieve the code verifier

In order to exchange the code for an access token we are going to need to have the code verifier that was used to generate the code challenge previously. Before redirecting to the authentication server I stored this in local storage so we'll need to retrieve that; `localStorage.getItem("verifier")`

#### Exchange the code

To exchange the code for an access token we need to make a `POST` request back to the authentication server on the `<authentication-server-url>/oauth/token` endpoint.

The request body needs to include some information to prove that this client should be granted an access token.

| parameter       | variable in example               | notes                                                                                                                      |
| --------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `grant_type`    | `"authorization_code"` hard coded | For PKCE this has to be `"authorization_code"`                                                                             |
| `client_id`     | `auth0ClientId`                   | The client id for the Auth0 client/application. This has to be the same as was used when requesting the code               |
| `code_verifier` | `codeVerifier`                    | The code verifier that was used to generate the code challenge in the previous step. This is retrieved from local storage. |
| `code`          | `code`                            | The code extracted from the query string                                                                                   |
| `redirect_uri`  | `authCallbackUrl`                 | The same callback url that was passed when requesting the user authenticate                                                |

The request body is going to look like this:

```ts
const res = await fetch(`${auth0TokenUrl}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    grant_type: "authorization_code",
    client_id: auth0ClientId,
    code_verifier: codeVerifier,
    code,
    redirect_uri: authCallbackUrl,
  }),
});
```

#### Read the response

A successful response from the authentication server will include a JSON response body that matches this signature:

```ts
type TokenResponse = {
  access_token: string;
  refresh_token: string;
  id_token: string;
  token_type: "Bearer";
  expires_in: number;
};
```

The last thing to do is to parse the fetch response. Here I'm writing it out to the tokens to the DOM, just to show that they've been received.

```ts
if (response.ok) {
  const resJson = (await response.json()) as TokenResponse;
  writeToDom(`<p>Got tokens ${JSON.stringify(resJson)}</p>`);
  return;
}
```

## Summing up

In this article I've gone through the steps to authenticate a user using PKCE using only the API's available in the browser. I said it at the top but I'll say it again, unless there is a solid reason to implement your own PKCE I'd strongly suggest using a library to take care of it for you. It's not worth committing to maintaining your own implementation when the libraries are fantastic.

> The example code for this blog can be found [on GitHub](https://github.com/KleeUT/2022_0_dep_pkce_client)