Using OAuth to protect Architect routes

avatar
Ryan Bethel
March 31, 2022

locked door Photo by Jornada Produtora on Unsplash

Don’t roll your own Auth is a thing people say. There is a lot of FUD-driven marketing around authentication in particular. People are rightly concerned about anything related to security. But there are ways to ease some of the pain and boilerplate of authentication while avoiding an expensive third-party authentication provider.

Architect 10 was just released with a new plugins API. It includes all the hooks needed to build an OAuth plugin.

When you start a new project, you often choose to ignore authentication at first to move quickly to solve unique problems. Moving fast is usually the right decision, but it is painful to go back and break a working app to add authentication after the fact. Ideally, you want to build your app with the hooks in place that can be used to upgrade to a secure solution.

The arc-oauth-plugin does just that. You can add it to an Architect project with a couple of lines of configuration. Then add a file with a few mock users, and you are on your way.

Overview

The plugin is a drop-in solution for implementing OAuth with a third-party provider (currently, it supports GitHub OAuth, but PRs are welcome for additional OAuth providers). It has built-in mocking for local development to use authentication immediately without setting up any API keys until you are ready to deploy. In addition to authentication, it includes basic authorization with a static list of approved accounts. The API also makes it easy to set up custom authorization (i.e., with users stored in a database).

The project repository has more documentation. An overview highlighting key features is below.

Basic Usage

For starting a project, use the following initial configuration to get started:

  1. Install: npm i arc-plugin-oauth

  2. Add the following lines to your manifest to enable mock authorization:

# app.arc
@plugins
arc-plugin-oauth

@oauth
use-mock true
allow-list
  1. Add a list of mock user accounts to the src/shared/mock-allow.mjs:
// src/shared/mock-allow.mjs
export default {
  mockProviderAccounts: {
    'Jane Doe': {
      login: 'janedoe',
      name: 'Jane Doe'
    },
    'John Smith': {
      login: 'johnsmith',
      name: 'John Smith'
    }
  },
  appAccounts: {
    janedoe: {
      role: 'member',
      name: 'Jane Doe'
    },
    johnsmith: {
      role: 'member',
      name: 'John Smith'
    }
  }
}

Mock OAuth Flow

The diagram below shows a typical OAuth user flow. When a user requests a protected page, they are redirected to a login page. This page has a link to jump to the 3rd party provider’s login page with an attached client ID. This link triggers the OAuth flow. Once that request is made to the provider and the user successfully logs in, they are redirected back to the app with a temporary code. The app server uses this code to request a long-lived token from a specific route. With this token, the authorized user can make requests to the provider. Only a single request is needed to get basic information about the user (i.e., name, email, etc.). The server identifies what application user account to connect using a unique field from this user info (i.e., username or email).

oauth flow chart with requests to and from third party provider

Some providers have slight variations of this flow. For instance, some require an initial request to a specific endpoint that returns values for all the other endpoints needed.

The plugin mocks the general flow for local development by creating a /mock/auth endpoint that intercepts all the provider requests. This means the server authorization logic works the same for mock and real authentication. The plugin sets environmental variables that determine the endpoints to point to in production and mock endpoints for local development.

Built-in Simple Authorization

Authorization is the compliment to authentication. Authentication checks if a user is who they say they are (i.e., Is this johnsmith@example.com). Authorization checks if they can do what they are requesting (i.e., is John Smith, a registered user who can publish). There is a built-in way to handle authorization for many use cases. First, configure the plugin for simple authorization by providing an allow list.

# app.arc
@plugins
arc-plugin-oauth

@oauth
use-mock true
allow-list allow.mjs

The plugin uses a static file that exports a list of users. This static list works for a small team or most situation where new users can be manually added to the approved user list in the file. An example allow-list is shown below:

// src/shared/allow.mjs
export default {
  appAccounts: {
    janedoe: {
      role: 'admin',
      name: 'Jane Doe'
    },
    johnsmith: {
      role: 'member',
      name: 'John Smith'
    }
  }
}

The keys in this exported map (i.e., ‘janedoe’) correspond to the unique property matched against the third-party provider.

Custom Authorization

The challenge is that authorization often requires database-backed user accounts that are tightly coupled to the implementation details of your app. No matter how many affordances are built into the plugin, you will frequently need to create your own authorization portion. The plugin helps by recommending an approach that will drop in with minimal changes. This can be done by:

Following these steps, everything works transparently, including the auth middleware and other helper functions. For more detail on this, refer to the repository readme.

Give it a try

This plugin is brand new. If you find any bugs please let me know. PRs and feedback are welcome. You are also welcome to fork it and make it your own. There is not a lot of code, and you can customize it to your way of building apps. If this plugin looks useful, try building a Functional Web App with it today (FWA.dev).