OAuth2 and AWS Cognito for Browser Extensions
Tags: javascript, extensions, webdev, beginners
Published: 2024-04-15 10:25:08

This is a guick guide on how to do OAuth2 logins within a chrome extension. Let's get started:

## Step 1: Register the Extension

OAuth2 requires a static URL to redirect the client after the authentication with the third party server is completed.
Since Browser Extensions are bound to a traditional URL, browsers rely on a trick.

Extensions can be registered to the Chrome Webstore and obtain a subdomain on `chromiumapps.org`:

```
<extension-id>.chromiumapps.org
```

After an OAuth2 flow is initiated, the browser will listen for redirects on that URL and expose the authentication code through a special API, more on this later.

To register the extension follow this [guide](https://developer.chrome.com/docs/extensions/how-to/integrate/oauth) from Google.

Before proceeding, make sure that your `manifest.json` contains the following fields:

```json
{
  "key": "-----BEGIN PUBLIC KEY-----...",
  "permissions": ["identity"]
}
```

## Step 2: Setup AWS Cognito

Assuming that you already have a Cognito User Pool,
the next step is to create a client application.

Set the redirect URL to the following:

```
https://<extension-id>.chromiumapps.org/
```

Take note of the Client ID issued by AWS Cognito and put it in the `manifest.json` of your extension.

```json
{
  "oauth2": {
    "client_id": "<your-client-id>",
    "scopes": ["email", "openid", "profile"]
  }
}
```

## Step 3: Write the Code

Here is how the auth flow will look like:

1. User installs the App
2. Browser opens a new tab with an auth page
3. User clicks on Login
4. Browser opens a popup window with cognito hosted ui
5. User logs in from the popup
6. Browser closes popup and show success message

### Open new tab on install

Add the following code to your background script:

```tsx
// background.js
browser.runtime.onInstalled.addListener(async () => {
  browser.tabs.create({
    url: browser.runtime.getURL("auth.html"),
  });
});
```

### Create login button

Add an `auth.html` page to your extension folder with a login button and a script tag pointing to `auth.js`

```html
<head>
  <title>Auth</title>
</head>
<body>
  <button id="login_button">Login</button>
  <script src="/auth.js"></script>
</body>
```

Connect the button to a `login` function that we are going to complete later.

```javascript
async function login() {
  console.log("Logging in...");
}

const loginButton = document.getElementById("login_button");

loginButton.addEventListener("click", () => {
  login().catch(console.error);
});
```

### Initiate OAuth2 flow

Let's complete the login function step by step.
First, let's define some variables to use as parameters for
the auth server:

```javascript
const manifest = browser.runtime.getManifest();

const AUTH_DOMAIN = "<your-cognito-server-domain";
const AUTH_CLIENT_ID = manifest.oauth2.client_id;
const AUTH_REDIRECT_URL = browser.getRedirectUrl("/");
const AUTH_RESPONSE_TYPE = "code"; // recommended
const AUTH_SCOPE = browser.oauth2.scopes.join(" ");
```

Then we use them to build an [`authorization`](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html) request:

```javascript
// https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
const authorizeUrl = new URL("oauth2/authorize", `https://${AUTH_DOMAIN}`);

authorizeUrl.searchParams.set("client_id", AUTH_CLIENT_ID);
authorizeUrl.searchParams.set("redirect_uri", AUTH_REDIRECT_URL);
authorizeUrl.searchParams.set("response_type", AUTH_RESPONSE_TYPE);
authorizeUrl.searchParams.set("scope", AUTH_SCOPE);
```

Now it's time to call a api that will open the popup for us and tell the browser to listen for a redirect to `<extension-id>.chromiumapps.org`.

```javascript
const redirectUrl = await browser.identity.launchWebAuthFlow({
  url: authorizeUrl.toString(),
  interactive: true,
});
```

If your have set `response_type` to `code`, the `redirectUrl`
variable should look like this:

```
https://<your-extension-id>.chromiumapp.org/?code=1234
```

We can easily get the code using the `URL` class:

```javascript
const authCodeUrl = new URL(redirectUrl);
const authCode = authCodeUrl.searchParams.get("code");
```

### Obtain a session token

Let's now exchange the code with a token using another endpoint from AWS Cognito:

```javascript
const tokenUrl = new URL("oauth2/token", `https://${AUTH_DOMAIN}`);

const tokenRes = await fetch(tokenUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    code: authCode,
    grant_type: "authorization_code",
    redirect_uri: AUTH_REDIRECT_URL,
    client_id: AUTH_CLIENT_ID,
  }),
});

if (!tokenRes.ok) {
  throw new Error("Failed to fetch token");
}

const token = await tokenRes.json();
```

This token can now be stored for future use.
Since we are inside a Browser Extension, the recommended approach is to use the [`browser.storage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage) api instead of `localStorage`.

```javascript
await browser.storage.local.set("token", token);
```

> Make sure to add the `storage` permission to your `manifest.json`

### Get user info

Now that we have a session token we can call the [`userInfo`](https://docs.aws.amazon.com/cognito/latest/developerguide/userinfo-endpoint.html) endpoint to retrieve the users email, username and other data depending on the setup.

```javascript
// auth.js

async function fetchUser() {
  // Retrieve token from storage
  const storageGetResult = await browser.storage.get("token");
  const token = storageGetResult["token"] as Token | undefined;
  if (!token) throw new Error("Not logged in.");

  // Fetch user info using the access token
  const userInfoUrl = new URL("oauth2/userInfo", `https://${AUTH_DOMAIN}`);
  const userInfoRes = await fetch(userInfoUrl, {
    headers: {
      Authorization: `Bearer ${token.access_token}`,
    },
  });

  const user = await userInfoRes.json();
  return user;
}
```

### Logout

Finally, let's write a function to log out the user by revoking the token and removing it from the local store:

```javascript
// auth.js

async function logout() {
  // Retrieve token from storage
  const storageGetResult = await browser.storage.get("token");
  const token = storageGetResult["token"];
  if (!token) throw new Error("Not logged in.");

  // Revoke token on the server using the refresh token
  const url = new URL("oauth2/revoke", `https://${AUTH_DOMAIN}`);
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      token: token.refresh_token,
      client_id: AUTH_CLIENT_ID,
    }),
  });

  if (!response.ok) throw new Error("Failed to revoke token");

  // Remove the token from storage
  await browser.storage.local.remove("token");
}
```

## Conslusions

And that's it. You should have all you need to develop a Browser Extension with OAuth2 and AWS Cognito.