API Auth for SPAs with Auth0 on Cloudflare Workers

K - Jul 24 '20 - - Dev Community

TL;DR Code on GitHub

Cloudflare allows us frontend devs to build APIs in no-time. They will be edge deployed and don't need an extra API gateway! As I already wrote in one of my previous articles, it's built on web technology, you know from the browser: JavaScript and Service workers.

This is a blessing and a curse. On the one hand, we can use the APIs we know from the browser; on the other hand, some Node.js packages won't work here, because the APIs are missing.

That's why I wrote this how-to.

Pre-Requisites

You need to sign up for Cloudflare and Auth0. They both come with a generous free plan, so no fear!

You also need a Linux or macOS system with a Node.js installation.

Cloning the GitHub Repo

API Auth for SPAs with Auth0 on Cloudflare Workers

This example illustrates API authentication for APIs that are hosted on Cloudflare workers.

More details can be found in this article.

Prerequisites

  • Cloudflare Account
  • Auth0 Account
  • Auth0 SPA application configured for your workers.dev subdomain
    • https://spa-api-auth.<YOUR_WORKERS_SUBDOMAIN>.workers.dev/index.html

Setup & Deploy

Please add your Cloudflare and Auth0 account details to the credentials.json before deployment.

$ npm i
$ npm start

View

The example can be viewed on your workers.dev subdomain.

https://spa-api-auth.<YOUR_WORKERS_SUBDOMAIN>.workers.dev/index.html



The repo contains a credentials.json where you need to add your Auth0 and Cloudflare account details and a setup.js that will sprinkle your credentials into your the files that need them. You can look at the tmp.* files to check what credentials are required for your projects later.

The Cloudflare apiKey can be found as Global API Key on your Cloudflare profile

The Cloudflare accountId can be found in the Cloudflare dashboard. You have to click on "Manage Workers" to get to it.

The Cloudflare email is the email address you signed up with to Cloudflare.

For the Auth0 credentials, you need to create an "Auth0 Application" first.

Creating an Auth0 Application

Create a "SPA Application" on the Auth0 Dashboard. There is a big orange button on the top right for that purpose.

The app name is not essential here. What is important is the "Domain" and "Client ID" that will be shown at the top of the settings after you created the app. These are the last two values needed for the credentials.json.

Next, update the application settings.

You have to add your Cloudflare workers.dev subdomain and URLs to the index.html so Auth0 knows which CORS headers it should use and where to redirect users after login.

  • Application Login URI
    • https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev/index.html
  • Allowed Callback URLs
    • https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev/index.html
  • Allowed Logout URLs
    • https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev/index.html
  • Allowed Web Origins
    • https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev
  • Allowed Origins (CORS)
    • https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev

Initializing & Deploying

Now that you set up everything on "The Cloud" side of things and linked it to your codebase, you're ready to deploy!

Run the following commands:

$ npm i
$ npm start
Enter fullscreen mode Exit fullscreen mode

This will do three things:

  1. Install Cloudflare's Wrangler CLI locally to the NPM project
  2. run the setup.js that will set all the credentials
  3. publish the generated index.js to Cloudflare Workers workers.dev domain

View the SPA

The SPA will be served via a Cloudflare Worker and can be accessed with the following URL:

https://spa-api-auth.<CF_ACCOUNT_NAME>.workers.dev/index.html
Enter fullscreen mode Exit fullscreen mode

Just use your Cloudflare account instead of the placeholder.

Interesting Code

The code I struggled with :D

Cloudflare Credentials

The Wrangler CLI works with environment variables, so you can set them before running it. I did this with a shell script.

#!/bin/bash
export CF_API_KEY=%CF_API_KEY%
export CF_EMAIL=%CF_EMAIL%
Enter fullscreen mode Exit fullscreen mode

Note: You have to run it with source ./setkeyenv.sh; otherwise, the variables will be gone when the next program is run in your shell.

Serving HTML with Cloudflare Workers

I didn't use Workers Sites here, the hosting product of Cloudflare. Instead, I integrated an HTML file as a string. This could also be done for other files like CSS, images, and JavaScript.

if (request.url.includes("/index.html"))
  return new Response(html, {
    headers: { "Content-Type": "text/html" }
  });
Enter fullscreen mode Exit fullscreen mode

Note: This only works if the string isn't too big, and you have to set the Content-Type header; otherwise, the browser won't recognize the HTML.

Getting a JWT from Auth0'S Lock Widget

The Lock widget would return something called an "opaque token" and not a JWT when I first run it. I had to supply it with the following config to get a JWT token. It will be stored inside authResult.idToken after an authentication happened.

const audience = location.protocol +
  "//" + location.hostname + "/api";
const lockWidget = new Auth0Lock(
  "%AUTH0_CLIENT_ID%",
  "%AUTH0_DOMAIN%", {
    auth: {
      responseType: "id_token",
      params: { scope: "openid", audience },
    },
  })
Enter fullscreen mode Exit fullscreen mode

Setting the Right HTTP Header

Since I created my API, I'm free to use whatever header field I like, but I choose the Authorization header field because it is standard.

fetch("/api/private", {
  headers: { Authorization: "Bearer " + idToken }
})
Enter fullscreen mode Exit fullscreen mode

Getting the Right Public Key from Auth0

Auth0 hosts the current keys for you, so you have to fetch them before validating them.

You get a list of keys from their /.well-known/jwks.json endpoint and have to find the one that corresponds to your JWT.

The key ID, or kid, is in the JWT header, so you have to extract it from there and use it to find the right key.

Then you have to use the WebCrypto API to convert that key to a public key.

const [rawHeader] = request.headers
    .get("Authorization")
    .substring(6)
    .trim()
    .split(".");
const { kid } = JSON.parse(atob(rawHeader));
const request = await fetch(
  "https://%AUTH0_DOMAIN%/.well-known/jwks.json"
);
const { keys } = await request.json();
const jwk = keys.find((key) => key.kid === kid);
const publicKey = crypto.subtle.importKey(
  "jwk",
  jwk,
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
  false,
  ["verify"]
);
Enter fullscreen mode Exit fullscreen mode

Note: Auth0's /.well-known/jwks.json endpoint is rate-limited, and Cloudflare Workers scale pretty good, so you need to cache them in production. The best place would be Workers KV.

Summary

Cloudflare Workers are the most light-weight FaaS system I know and are even edge deployed by default. But they are also different from Node.js, which required me to jump through some hoops to get things running.

Auth0, on the other hand, is a high level managed serverless auth service that does much of the heavy lifting for you, so while finding the public keys wasn't that straight forward, it saves so much time, you can't quite imagine.

In the end, everything worked out, and I'm happy with the result.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .