Skip to content

byuweb/byu-browser-oauth-pkce

Repository files navigation

byu-browser-oauth-pkce

OAuth PKCE Grant provider for byu-browser-oauth.

For questions or issues, try the issue tracker or the Web Community Slack.

Usage

About OAuth PKCE Grant flow

In a normal OAuth authentication flow, the client calls the authentication server with a client id and a client secret, which are roughly analagous to a username and password. When working with distributed clients, such as Javascript-based browser applications, we can't use the client secret, as that would expose it to the world and allow anyone to make calls as our application.

PKCE Grant aims to solve this problem by using your application's URL instead of the secret. The theory is that, because URLs are guaranteed to be unique, no other application can impersonate yours.

When you register your application with the OAuth server, you must provide a 'callback URL'. Note that this does NOT mean that you must register every possible URL in your application - you just need to register one path in your application that the OAuth server will send users to after logging them in. Generally, this is the root URL of your application, but you can use any other URL that is unique to your app.

For non-production environments, you should always use a 'Sandbox' client ID. For the production environment, use the 'Production' key.

You can register your application and callback URLs in the api.byu.edu/store. For each environment in your application (prod, nonprod, local, etc.), you must take the following steps:

  1. Create an application
  2. Generate keys (even though you only need the client ID.)

Using PKCE Grant in your application

The PKCE Grant OAuth provider is available from the Web Community CDN. It is preferred that you not bundle the provider into your application, so that you can benefit from updates to the provider (including security updates) without changing your application. The Web Community CDN is built to be fast and efficient, and serves millions of requests reliably.

The provider is distributed as a Javascript Modules. ES Modules are supported by all browsers that the Web Community supports (latest 2 versions of Chrome, Safari, Firefox, and Edge). There is also a non-module build available, though support for it may be limited.

The easiest way to import the provider into your application is to import it in the <head> of your page. If you are building your application to output ES Modules, you can also choose to import it into your application's main Javascript files. However, this will not work if you build transpile your application for use on browsers that do not support modules, so it's probably easiest to put it in your <head>.

However you import it, the Javascript you use to initialize the provider will be similar:

import * as pkce from 'https://cdn.byu.edu/browser-oauth-pkce/latest/pkce-grant.min.js';

pkce.configure({
  // your configuration here (see below) 
});

Your configuration block can take one of two forms:

  • URL-to-configuration mappings (recommended)
  • Plain configuration object (only if you know what you're doing)

Configuration Options

The main configuration object has the following options:

Name Type Default Description
clientId String None (required) The OAuth client ID
callbackUrl URL String Current URL The callback URL registered to your application
autoRefreshOnTimeout Boolean false Whether to try to automatically refresh the user's session when it expires
logoutRedirect URL String Current URL Where the user's browser should redirect after completing logout process
issuer URL String https://api.byu.edu The OAuth issuer to use. Do not change this unless you know what you are doing.
baseUrl URL String https://api.byu.edu The base URL for authentication. Use "https://api-sandbox.byu.edu" for Sandbox
const config = {
  clientId: 'client ID',
  callbackUrl: 'http://my-app.byu.edu',
  autoRefreshOnTimeout: true
};

If you know exactly what settings should be used for this environment of your application, you can pass this object directly to configure. Otherwise, you should use the URL-matching configuration.

URL-matching configuration

In this form, you map URL patterns to configuration objects.

const config = {
  'https://my-app.byu.edu': {
    clientId: 'production client ID'
  },
  'https://stg.my-app.byu.edu': {
    clientId: 'stage client ID'
  },
  'http://localhost:8080': {
    clientId: 'local development client ID'
  }
};

With the URL mappings, you specify what your settings are (including client IDs) for different URLs. The provider will select the appropriate configuration for the current page, matching the most-specific URL that is a subset of the current page's URL.

So, if the current page is https://example.com/my/application/nested/path, the following patterns will match:

  • https://example.com
  • https://example.com/
  • https://example.com/my
  • https://example.com/my/
  • https://example.com/my/application
  • https://example.com/my/application/
  • https://example.com/my/application/nested
  • https://example.com/my/application/nested/
  • https://example.com/my/application/nested/path

These will not match:

  • http://example.com (http instead of https)
  • https://subdomain.example.com (domain name must match exactly)
  • https://example.com/my-application (wrong path)
  • https://example.com/my/application/nested/path/stuff (too specific of a path)

The provider will always select the most specific URL pattern, so if the browser is at https://example.com/my/application/route, then a pattern of https://example.com/my/application will be preferred, even if there are configurations for https://example.com/my and https://example.com.

Note that, for purposes of resolving configurations, all query (?), fragment (#), and matrix (;) parameters are ignored.

Importing in your HTML

<head>
    <script type="module">
        import * as pkce from 'https://cdn.byu.edu/browser-oauth-pkce/latest/pkce-grant.min.js';

        pkce.configure({
            'https://my-app.byu.edu': { clientId: '{production key}' },
            'https://stg.my-app.byu.edu': { clientId: '{stage key}' },
            'http://localhost:8080': { clientId: '{local development key}' }
        });
    </script>
</head>

Interacting with the OAuth Provider

In order to get the authentication status, user information, and OAuth tokens, you can use a AuthenticationObserver. This is a provider-agnostic interface for handling OAuth sessions and user information.

For detailed, authoritative documentation, visit https://github.com/byuweb/byu-browser-oauth.

Installing

Unlike the PKCE grant provider, the AuthenticationObserver API is installed from NPM:

npm install --save @byuweb/browser-oauth

Or, if you use Yarn:

yarn add @byuweb/browser-oauth

In your Javascript code, you can then import the module and create an AuthenticationObserver:

// This import may need to be adjusted, depending on how you build your application
import { AuthenticationObserver } from '@byuweb/browser-oauth';
// If using Node-style imports: `const { AuthenticationObserver } = require('@byuweb/browser-oauth');

const observer = new AuthenticationObserver(({state, token, user, error}) => {
  // React to the change in state
  if (error) {
    // React to authentication error
  } else if (token && user) {
    // User is logged-in - start loading data or take other actions
  } else {
    // No user is logged in, but there has not been an error
  }
});

Every time the authentication state changes, your observer will receive a callback.

The state parameter contains a string value describing the current authentication state. You usually won't need to use this.

If the user is logged in, the token and user parameters will be set.

If set, token contains the current OAuth token.

If set, user contains information about the logged-in user.

If an error occurs in the login process, the error parameter will be set, containing the error. Generally, if there is an error, neither token nor user will be set.

Logging the user in or out

In order to log the user in, you must import and call the login and logout functions:

import { login, logout } from '@byuweb/browser-oauth';
// If using Node-style imports: `const { login, logout } = require('@byuweb/browser-oauth');

// When you want to log the user in:
login().then(({state, token, user, error}) => {
  // If we don't have to redirect the browser to log the user in, you can respond to the completed
  //  login here
});

// Similarly, for logout:
logout().then(({state, token, user, error}) => {
  // If we don't have to redirect the browser to log the user in, you can respond to the completed
  //  login here
});

If you application requires that users always be logged in, add this after instantiating your AuthenticationObserver:

import { AuthenticationObserver, login, STATE_UNAUTHENTICATED } from '@byuweb/browser-oauth';

const observer = new AuthenticationObserver(({state, token, user, error}) => {
  // React to the change in state
  if (error) {
    // React to authentication error
  } else if (token && user) {
    // User is logged-in - start loading data or taking other actions
  } else {
    if (state == STATE_UNAUTHENTICATED) {
      // User is not logged in AND authentication process has NOT already begun
      login();
    }
  }
});

A simpler way to require logins is coming soon.

Cleaning up the observer

If you wish to stop receiving notifications about authentication events, call disconnect() on AuthenticationObserver.

Debugging

To turn on debug logging, add the following Javascript snippet before you initialize the PKCE grant provider:

(window.byuOAuth = window.byuOAuth || {}).logging = 'debug';

You can also add an attribute to your page's <html> element. You do not need to do both.

<!DOCTYPE html>
<html lang="en" byu-oauth-logging="debug">
  <head></head>
  <body></body>
</html>