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:
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:
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.
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.
npx wrangler init gauth
Select y
to all the following:
š Click to copy1⨠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 copy1import GoogleAuth, { GoogleKey } from 'cloudflare-workers-and-google-oauth'
Now, let's provide the appropriate type for our environment variable.
š Click to copy1... 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 copy1... 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 copy1export 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
Deployment is extremely easy with Wrangler.
wrangler login
wrangler publish
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.
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:
{"alg":"RS256","typ":"JWT"}
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
.
https://oauth2.googleapis.com/token
Let's look at an example:
š Click to copy1{ 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}
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"}
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 copy1const 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})
Once you make the request successfully, you will get back a JSON blob of data:
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.