DEV Community

ken-miyashita
ken-miyashita

Posted on

Developing Chrome Extensions with Amplify Authentication

Title

Introduction

Chrome extensions are small programs that allow users to customize their browsing experience. Users can customize Chrome functionality and behavior in many ways with them. An extension might overlay information related to the currently displayed web page, for instance.

Personalized behaviors may be desired when you create Chrome extensions. Displaying information according to the user’s preferences, for example. In this case, your Chrome extension must manage user-specific data on your server. Now it’s time to introduce AWS Amplify. AWS Amplify provides a set of tools and features that lets frontend web and mobile developers quickly and easily build full-stack apps on AWS, with the flexibility to leverage the full breadth of AWS services as your use cases change.

This article explains how you can create Chrome extensions combining the following technologies.

  • React
  • TypeScript
  • AWS Amplify
  • AWS Cognito (Authentication)
  • AWS AppSync (GraphQL)

I have created a boilerplate git repository, showing a working example.

Prerequisites

The following basics are not covered in this article, and you are expected to be familiar with them. Since those technologies provide comprehensive tutorials, I recommend you go through them first.

Chrome extension

  • What are (1) background scripts (2) popup scripts, and (3) content scripts.
  • How those scripts are segmented, and how they communicate with each other.

Webpack

  • Why bundling is necessary.
  • What are (1) entry points, (2) bundles, and (3) loaders.

AWS Amplify (with react)

  • How to develop React applications with Amplify.
  • How to integrate Amazon Cognito as the main authentication provider.
  • How to connect API and database to your applications.

What Are Challenges?

You can find many tutorials and working examples online if you want to create simple Chrome extensions or standalone web applications with Amplify. However, if you want to combine the above technologies, you would encounter the following challenges. The solution may seem obvious after reading this article. But I spent a few weeks trying to achieve a stable codebase just combining them, and I believe my boilerplate would be helpful as a starter.

Folder structure

Multiple small scripts (applications) are working together, sharing code auto-generated by Amplify. What is the most appropriate folder structure for them?

Webpack configuration for Chrome extensions with react

Typically, create-react-app is used when creating react applications. It is a great tool and gives you a solid starting point to develop full-fledged react applications. But you cannot use the outcome of create-react-app as is for your Chrome extensions. Although we need to create multiple bundles (background script, popup script, content script), create-react-app does not support that use case.

Managing security tokens for AWS authentication

By default, AWS Amplify stores security tokens in localStorage for the browser. But if you want to sign in on your Chrome extension’s popup window and let the content script access personalized data, this default behavior is inconvenient. You need to establish the way to manage security tokens shared among popup scripts and content scripts.

Chrome Extension Example (Boilerplate)

Overview

The boilerplate is a fully functional (but minimal) application with the following features.

  • Portal site: It is a simple react application accessing personalized data (ToDo items) on AWS.
  • Popup script: It allows users to sign in to AWS. Security tokens are stored in Chrome storage.
  • Content Scripts: With security tokens in Chrome storage, content scripts access personalized data on AWS.

Architecture

Setup

Clone the boilerplate repo, and install dependencies.

$ git clone https://gitlab.com/kmiyashita/chrome-extension-amplify-auth.git
$ cd chrome-extension-amplify-auth
$ yarn
Enter fullscreen mode Exit fullscreen mode

Move to the sub folder for Amplify project and initialize the project.

$ cd packages/amplify-shared
$ amplify init
? Choose your default editor: Visual Studio Code
? Select the authentication method you want to use: AWS 
profile
? Please choose the profile you want to use:  default
Enter fullscreen mode Exit fullscreen mode

Lastly provision the backend resources using the config files in the amplify directory.

$ amplify push
Enter fullscreen mode Exit fullscreen mode

Run Chrome Extension

Build Chrome Extension.

$ yarn build
Enter fullscreen mode Exit fullscreen mode

Open the Extension Management of Chrome browser, and load the extension built in chrome-extension-amplify-auth/packages/chrome-ext/dist

Extension

When you open any web page, you’ll notice that your content script shows a small overlay at the lower right corner.

Sign in

By clicking on the Chrome extension icon, you can open a pop-up window. Create a new user account, and sign in.

Image description

Now, the overlay by the content script automatically updates, and shows ToDo items.

Image description

Run Portal Site

Run the web server for the portal site.

$ yarn start-web
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080 in Chrome browser, and observe ToDo items.

Image description

Folder Structure

We have the following two requirements regarding folder structure.

  • Amplify code should be shared among multiple bundles in Chrome extension (content script, popup script) and Portal site.
  • Multiple bundles (content script, popup script, background script) need to be created for Chrome extension.

In order to meet those requirements, we take the following approach.

  • Monorepo structure.
  • Amplify project forms an independent package (“amplify-shared”) in monorepo structure. This package is shared among two packages: “chrome-ext” and “web-ui”.
  • The package “chrome-ext” has three sub folders for background script, content script, and popup script. Webpack creates bundles from those sub folders.

Image description

Webpack Configuration for Chrome Extension

Webpack is a static module bundler for modern JavaScript applications. In most cases, webpack does not require a configuration file to bundle your project since version 4.0.0. But we would introduce the minimal configuration file webpack.config.js for the following purposes.

  • Create multiple bundles (background script, popup script, content script)
  • Compile TypeScript codes into JavaScript.
  • Suppress using “eval” for source mapping since Chrome extension’s CSP (Content Security Policy) does not allow it.

Image description

Authentication

When you sign in using Authentication with AWS Amplify, security tokens are stored in localStorage by default. After signing in, Amplify functions to access data use the stored security tokens.

Unfortunately, in our scenario, this mechanism doesn’t work as intended. Popup script runs in the extension’s context while content scripts run in the context of a web page and not the extension. Since localStorage is segmented per context, security tokens stored by popup script cannot be accessed by content scripts.

But Amplify is well designed, and it allows us to customize where security tokens are stored. In order to solve this problem, we use chrome.storage.local. It provides the same storage capabilities as the localStorage API with the following key differences:

  • Your extension’s content scripts can access user data in common shared with the popup script.
  • It’s asynchronous with bulk read and write operations, and therefore faster than the blocking and serial localStorage API.

Here is SharedAuthStorage.ts implementing our custom storage for security tokens.

In the popup script

  • setItem() is called and security tokens are stored in chrome.storage.local.

In the content script

  • sync() is called in Amplify.configure(). It populates on-memory cache, scanning chrome.storage.local.
  • Once on-memory cache is populated, getItem() can return values (security tokens) synchronously.
const CHROME_STORAGE_KEY_PREFIX = 'AmplifyStorage-';

/**
 * Enumerate all relevant key-value items in chrome.storage.local.
 * @param operator - operator to apply on items
 */
function enumerateItems(operator) {
  chrome.storage.local.get(null, (items) => {
    const chromeStorageKeys = Object.keys(items).filter((key) => key.startsWith(CHROME_STORAGE_KEY_PREFIX));
    chrome.storage.local.get(chromeStorageKeys, (items => {
      // items is an object which has key-value.
      // Each key has a prefix, and you need to remove it if you want to access on-memory cache.
      operator(items);
    }));
  });
}

export default class SharedAuthStorage {
  static syncPromise: Promise<void> | null = null;
  static cache = new Map();

  static setItem(key:string, value:string) {
    chrome.storage.local.set({[CHROME_STORAGE_KEY_PREFIX + key]: value});
    SharedAuthStorage.cache.set(key, value);
  }

  static getItem(key:string) {
    let value = null;
    if (SharedAuthStorage.cache.has(key)) {
      value = SharedAuthStorage.cache.get(key);
    }
    return value;
  }

  static removeItem(key: string) {
    chrome.storage.local.remove(CHROME_STORAGE_KEY_PREFIX + key);
    SharedAuthStorage.cache.delete(key);
  }

  static sync() {
    if (!SharedAuthStorage.syncPromise) {
      SharedAuthStorage.syncPromise = new Promise<void>((res) => {
        enumerateItems(items => {
          for (const [chromeStorageKey, value] of Object.entries(items)) {
            const key = chromeStorageKey.replace(CHROME_STORAGE_KEY_PREFIX, '');
            SharedAuthStorage.cache.set(key, value);
          }
          res();
        });
      });
    }
    return SharedAuthStorage.syncPromise;
  }
}
Enter fullscreen mode Exit fullscreen mode

You can configure Amplify to use this custom storage as follows. In this way, you combine your customization and various AWS configuration parameters (awsExports) managed by Amplify CLI.

Amplify.configure({
    ...awsExports,
    Auth: {storage: SharedAuthStorage}
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article explains how to use Amplify with authentication in your Chrome extensions. Hope the boilerplate will help your development.

Discussion (0)