Skip to content

Instantly share code, notes, and snippets.

@danbars
Last active August 5, 2024 09:27
Show Gist options
  • Save danbars/d39bad619db29669cebccf464d9e66e5 to your computer and use it in GitHub Desktop.
Save danbars/d39bad619db29669cebccf464d9e66e5 to your computer and use it in GitHub Desktop.
Generate Google access token for service-account in Cloudflare workers environment
/**
You can use the code below in Cloudfalre worker environment to generate a Google access token
for a service-account.
Run this code in a worker cron so you always have a fresh token in your KV that can be used by other workers.
The generated token here will be valid for 3600 seconds, but you can change that in your code.
The code assumes that you have a KV mapped to 'PROPERTIES' in you wrangler.toml file
You must create a service-user and assign to it the roles that you need in google console.
Then take the JSON that you get and paste it below
This code also assumes that you have your private key mapped to a variable `PRIVATE_KEY`
Note that currently `wrangler secret put PRIVATE_KEY` will not be able to save private keys because they are too long.
Instead, use Cloudflare dashboard UI to save this as an environemnt variable for your worker
**/
import jwt from 'jsonwebtoken';
/* global PRIVATE_KEY, PROPERTIES */
const ServiceAccountJson = {
"type": "service_account",
"project_id": "project-id",
"private_key_id": "92c482736826384a88",
"private_key": PRIVATE_KEY,
"client_email": "[email protected]",
"client_id": "87263847628736478",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-user%40project-id.iam.gserviceaccount.com"
}
addEventListener('scheduled', event => {
event.waitUntil(handleScheduled())
})
//store it under whatever key that you want.
//Use the permission scope that you need for your access token
async function handleScheduled() {
await generateKey(ServiceAccountJson, 'GOOGLE_FIRESTORE_ACCESS_TOKEN', 'https://firestore.googleapis.com/')
}
async function generateKey(serviceAccount, property, aud) {
const now = new Date()
const NOW = Math.floor( now.getTime() / 1000);
const token = jwt.sign({
iss: serviceAccount.client_email,
sub: serviceAccount.client_email,
aud,
iat: NOW,
exp: NOW + 3600
},
serviceAccount.private_key,
{
algorithm: 'RS256',
keyid: serviceAccount.private_key_id
});
//store token in KV
await PROPERTIES.put(property, token)
console.log('generated token', property)
}
@Mike-Jagger
Copy link

Mike-Jagger commented Aug 4, 2024

This helped me BIG time to write a more one off function that I call in one of my workers (ignoring the storage part)

UPDATE:

My old implementation of a custom function to generate a signed JWT, to make api calls, to firestore will still throw the same error if you try to call it (when testing previously, I didn't make any implement a unit test for the function so my bad.)

Apparently, Cloudflare doesn't support the jsonwebtoken package anymore as highlighted in this issue, so I used jose instead to perform the signing.

Here is the new implementation:

import { SignJWT, importPKCS8 } from 'jose';

export async function generateFirestoreAccessToken(env) {
    const serviceAccountKey = JSON.parse(atob(env.FIRESTORE_SERVICE_ACCOUNT_KEY));
    const aud = "https://firestore.googleapis.com/";

    const header = {
        "alg": "RS256",
        "typ": "JWT",
        "kid": serviceAccountKey.private_key_id,
    }

    const privateKey = await importPKCS8(serviceAccountKey.private_key, header.alg);

    const NOW = Math.floor(new Date().getTime() / 1000);

    const payload = {
        iss: serviceAccountKey.client_email,
        sub: serviceAccountKey.client_email,
        aud,
        iat: NOW,
        exp: NOW + 3600,
    };

    const jwt = await new SignJWT(payload)
        .setProtectedHeader(header)
        .sign(privateKey);

    return jwt;
}

Note: The environment variable here, FIRESTORE_SERVICE_ACCOUNT_KEY, is the key you get after creating your service account and granting roles to it. In my case, I gave it full access to firebase via the FIREBASE ADMIN role (not the ADMIN SDK.) There are probably better access levels that can be given [, will research on it later]. Also note that I encoded the contents of the json file to base64 before storing it as an env variable.

Resources:

Hope this helps!

ORIGINAL IMPLEMENTATION

export async function getFirestoreAccessToken(env) {
    const jwt = await import('jsonwebtoken');
  
    const serviceAccountKey = JSON.parse(atob(env.FIRESTORE_SERVICE_ACCOUNT_KEY));
    const aud = "https://firestore.googleapis.com/";
  
    const now = new Date()
    const NOW = Math.floor( now.getTime() / 1000);
    const token = jwt.sign({
      iss: serviceAccountKey.client_email,
      sub: serviceAccountKey.client_email,
      aud,
      iat: NOW,
      exp: NOW + 3600
    },
    serviceAccountKey.private_key,
    {
      algorithm: 'RS256',
      keyid: serviceAccountKey.private_key_id
    });
  
    return token;
} 

I don't know why but when I import the jsonwebtoken package from outside the function synchronously, I get this error:

Error: Module cannot be synchronously required while it is being instantiated or evaluated. This error typically means that a CommonJS or
NodeJS-Compat type module has a circular dependency on itself, and that a synchronous require() is being called while the module is being loaded.
 ❯ ../../node_modules/semver/classes/range.js?mf_vitest_no_cjs_esm_shim:219:20
 ❯ ../../node_modules/semver/classes/comparator.js?mf_vitest_no_cjs_esm_shim:141:15
 ❯ ../../node_modules/semver/index.js?mf_vitest_no_cjs_esm_shim:29:20
 ❯ ../../node_modules/jsonwebtoken/lib/asymmetricKeyDetailsSupported.js?mf_vitest_no_cjs_esm_shim:1:16
 ❯ ../../node_modules/jsonwebtoken/lib/validateAsymmetricKey.js?mf_vitest_no_cjs_esm_shim:1:42
 ❯ ../../node_modules/jsonwebtoken/verify.js?mf_vitest_no_cjs_esm_shim:6:31
 ❯ ../../node_modules/jsonwebtoken/index.js?mf_vitest_no_cjs_esm_shim:3:11

reason why I imported it using the async import syntax

Note: I was using vitest to run my tests

@danbars
Copy link
Author

danbars commented Aug 5, 2024

Since this code was written I actually moved to @tsndr/cloudflare-worker-jwt which seems to be well maintained and works really well for me in Cloudflare workers environment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment