Ryan Schachte.
+
OAuth2 and Cloudflare Workers
profile
written by: Ryan SchachteOctober 21st, 2022

Background

Cloudflare Workers provide a wonderful alternative for deploying applications on the edge in a fast, cheap and reliable way. One thing I've found painful is communicating to Google Cloud Platform from within a Cloudflare Worker. This is mainly because there is a lot involved when generating the signed JWT for authenticating to the Google servers.

After being inspired by this Github Gist I've developed a Google OAuth2 module for Cloudflare Workers to help ease the pain when authenticating to the G.

Let's break down the 5 steps:

  1. Create Google Cloud service account
  2. Create a JSON key for the account
  3. Upload encrypted key to Cloudflare Worker
  4. Implement auth library
  5. Deploy

Implementation

Google Cloud Setup

Let's begin by creating a service account on Google Cloud. The service account will be used on the Cloudflare Worker to authenticate itself to Google Cloud.

Navigate to:

  • IAM
    • Service Accounts
      • Create service account

From here, simply fill out the form and apply the permissions/scopes you want the account to have access to.

Within the service account page, select keys -> Add Key -> Create new key. Download the JSON file for this key and keep it somewhere safe.

Cloudflare Setup

On the Cloudflare dash, navigate to the Workers page and create a new worker. Create a new environment variable and name it GCP_SERVICE_ACCOUNT. The value will be the contents of the JSON key downloaded in the previous step.

Create a new Typescript Worker

npx wrangler init gauth

Select y to all the following:

šŸ“‹ Click to copy
1āœØ Created gauth/wrangler.toml 2Would you like to use git to manage this Worker? (y/n) 3āœØ Initialized git repository at gauth 4No package.json found. Would you like to create one? (y/n) 5āœØ Created gauth/package.json 6Would you like to use TypeScript? (y/n) 7āœØ Created gauth/tsconfig.json 8Would you like to create a Worker at gauth/src/index.ts?

Next, we will install the Google OAuth2 module for authenticating the Worker.

npm i cloudflare-workers-and-google-oauth

This module will give us the ability to generate a service account authentication token on the fly from within a Worker request path.

Let's crack open src/index.ts.

The first thing we'll do is import the library and associated interface.

šŸ“‹ Click to copy
1import GoogleAuth, { GoogleKey } from 'cloudflare-workers-and-google-oauth'

Now, let's provide the appropriate type for our environment variable.

šŸ“‹ Click to copy
1... 2 3// Add secret using Wrangler or the Cloudflare dash 4export interface Env { 5 GCP_SERVICE_ACCOUNT: string; 6} 7 8export default { 9 async fetch( 10 request: Request, 11 env: Env, 12 ctx: ExecutionContext 13 ): Promise<Response> { 14};

Now that we have the skeleton of the Worker built, let's generate our OAuth token.

šŸ“‹ Click to copy
1... 2 3export default { 4 async fetch( 5 request: Request, 6 env: Env, 7 ctx: ExecutionContext 8 ): Promise<Response> { 9 // https://developers.google.com/identity/protocols/oauth2/scopes 10 const scopes: string[] = ['https://www.googleapis.com/auth/devstorage.full_control'] 11 const googleAuth: GoogleKey = JSON.parse(env.GCP_SERVICE_ACCOUNT) 12 13 // Initialize the service 14 const oauth = new GoogleAuth(googleAuth, scopes) 15 const token = await oauth.getGoogleAuthToken() 16 }, 17}; 18

We define the scopes we want to authorize for. This is essentially the IAM roles that grant the account access to services on GCP.

We instantiate a new instance of the OAuth library and give it the parsed key and defined scopes. From here, we can request tokens on the fly per request. Let's show how we could retrieve an image from Google Cloud Storage.

šŸ“‹ Click to copy
1export default { 2 async fetch( 3 request: Request, 4 env: Env, 5 ctx: ExecutionContext 6 ): Promise<Response> { 7 // https://developers.google.com/identity/protocols/oauth2/scopes 8 const scopes: string[] = ['https://www.googleapis.com/auth/devstorage.full_control'] 9 const googleAuth: GoogleKey = JSON.parse(env.GCP_SERVICE_ACCOUNT) 10 11 // Initialize the service 12 const oauth = new GoogleAuth(googleAuth, scopes) 13 const token = await oauth.getGoogleAuthToken() 14 15 // Example with Google Cloud Storage 16 const res = await fetch('https://storage.googleapis.com/storage/v1/b/MY_BUCKET/o/MY_OBJECT.png?alt=media', { 17 method: 'GET', 18 headers: { 19 'Authorization': `Bearer ${token}`, 20 'Content-Type': 'image/png', 21 'Accept': 'image/png', 22 }, 23 }) 24 25 return new Response(res.body, { headers: { 'Content-Type': 'image/png' } }); 26 }, 27};

As you can see, in just a few lines of code, you are already downloading images and videos from your Google Cloud account and serving them up via a Cloudflare Worker.

View the Github repository here

Deploy

Deployment is extremely easy with Wrangler.

  1. wrangler login

  2. wrangler publish

Understanding the internals

We will walk through the core ideas behind the OAuth2 flow, how it works and how we're retrieving refreshed tokens on the fly on each request.

What is JWT?

JWT (pronounced jot) is a token standard composed of 3 parts.

{"<HEADER>.<CLAIM_SET>.<SIGNATURE>"}

Note: Each of these units are base64 encoded independent of one another.

The header is JSON and contains 2 fields:

  • The signing algorithm
  • The data format

{"alg":"RS256","typ":"JWT"}

Claim set

The claim set in the JWT has information about the token itself. That information can be things such as the scopes being requested (ie. cloud storage access, VM access, IAM access), the token issuance time, who issued it, token lifespan, etc.

Let's briefly touch on the required fields notes in the Google OAuth documentation.

  • iss: email address of the service account
  • scope: space delimited set of permissions that the app has
  • aud: assertion descriptor https://oauth2.googleapis.com/token
  • exp: expiration time
  • iat: issuance time

Let's look at an example:

šŸ“‹ Click to copy
1{ 2 "iss": "[email protected]", 3 "scope": "https://www.googleapis.com/auth/devstorage.read_only", 4 "aud": "https://oauth2.googleapis.com/token", 5 "exp": 1444445555, 6 "iat": 1144444555 7}

Signature

The signature prevents tampering, as the tokens can be passed around in public. In order to generate a signature, we only need the first two components of our JWT.

createSignature(base64(header) + "." + base64(claimSet))

The signing header of the JWT must be RSA using the SHA-256 hashing algorithm.

Example: {"alg":"RS256","typ":"JWT"}

Request access token

Now that we have our computed JWT from above, we can make the access token request against the OAuth server from Google.

Note the following URL: https://oauth2.googleapis.com/token

This will be a POST request and require the following parameters:

  • grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
  • assertion: This is the full JWT including the signature
šŸ“‹ Click to copy
1const jwtUnsigned = `${jwtHeader}.${claimset}` 2const signedJwt = `${jwtUnsigned}.${await this.sign(jwtUnsigned, key)}` 3const body = `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${signedJwt}` 4 5const response = await fetch(this.googleKey.token_uri, { 6 method: 'POST', 7 headers: { 8 'Content-Type': 'application/x-www-form-urlencoded', 9 'Cache-Control': 'no-cache', 10 Host: 'oauth2.googleapis.com', 11 }, 12 body, 13})

Using the token

Once you make the request successfully, you will get back a JSON blob of data:

  • access_token
  • allowed scopes or services
  • the type of the token (ie Bearer)
  • when the access token will expire

From here, you can make requests to any service like Google Cloud Storage and just ensure there is a header present in the form of:

Authorization: Bearer access_token

Thanks for reading and shoutout to @markelliot and @Moumouls for inspiring me to write this.

2023 - site designed, coded and hosted by
profile
Ryan SchachteSanta Barbara, CA