OAuth2 and AWS Cognito for Browser Extensions

fibonacid

Lorenzo Rivosecchi

Posted on April 15, 2024

OAuth2 and AWS Cognito for Browser Extensions

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
Enter fullscreen mode Exit fullscreen mode

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 from Google.

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

{
  "key": "-----BEGIN PUBLIC KEY-----...",
  "permissions": ["identity"]
}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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

{
  "oauth2": {
    "client_id": "<your-client-id>",
    "scopes": ["email", "openid", "profile"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Create login button

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

<head>
  <title>Auth</title>
</head>
<body>
  <button id="login_button">Login</button>
  <script src="/auth.js"></script>
</body>
Enter fullscreen mode Exit fullscreen mode

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

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

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

loginButton.addEventListener("click", () => {
  login().catch(console.error);
});
Enter fullscreen mode Exit fullscreen mode

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:

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(" ");
Enter fullscreen mode Exit fullscreen mode

Then we use them to build an authorization request:

// 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);
Enter fullscreen mode Exit fullscreen mode

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.

const redirectUrl = await browser.identity.launchWebAuthFlow({
  url: authorizeUrl.toString(),
  interactive: true,
});
Enter fullscreen mode Exit fullscreen mode

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

https://<your-extension-id>.chromiumapp.org/?code=1234
Enter fullscreen mode Exit fullscreen mode

We can easily get the code using the URL class:

const authCodeUrl = new URL(redirectUrl);
const authCode = authCodeUrl.searchParams.get("code");
Enter fullscreen mode Exit fullscreen mode

Obtain a session token

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

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();
Enter fullscreen mode Exit fullscreen mode

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 api instead of localStorage.

await browser.storage.local.set("token", token);
Enter fullscreen mode Exit fullscreen mode

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 endpoint to retrieve the users email, username and other data depending on the setup.

// 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;
}
Enter fullscreen mode Exit fullscreen mode

Logout

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

// 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");
}
Enter fullscreen mode Exit fullscreen mode

Conslusions

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

💖 💪 🙅 🚩
fibonacid
Lorenzo Rivosecchi

Posted on April 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related