Skip to content

Instantly share code, notes, and snippets.

@markelliot
Last active March 20, 2025 19:22
Show Gist options
  • Save markelliot/6627143be1fc8209c9662c504d0ff205 to your computer and use it in GitHub Desktop.
Save markelliot/6627143be1fc8209c9662c504d0ff205 to your computer and use it in GitHub Desktop.
Converts Google service user OAuth2 credentials into an access token in Cloudflare-compatible JS
/**
* Get a Google auth token given service user credentials. This function
* is a very slightly modified version of the one found at
* https://community.cloudflare.com/t/example-google-oauth-2-0-for-service-accounts-using-cf-worker/258220
*
* @param {string} user the service user identity, typically of the
* form [user]@[project].iam.gserviceaccount.com
* @param {string} key the private key corresponding to user
* @param {string} scope the scopes to request for this token, a
* listing of available scopes is provided at
* https://developers.google.com/identity/protocols/oauth2/scopes
* @returns a valid Google auth token for the provided service user and scope or undefined
*/
async function getGoogleAuthToken(user, key, scope) {
function objectToBase64url(object) {
return arrayBufferToBase64Url(
new TextEncoder().encode(JSON.stringify(object)),
)
}
function arrayBufferToBase64Url(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_")
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
};
async function sign(content, signingKey) {
const buf = str2ab(content);
const plainKey = signingKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace(/(\r\n|\n|\r)/gm, "");
const binaryKey = str2ab(atob(plainKey));
const signer = await crypto.subtle.importKey(
"pkcs8",
binaryKey,
{
name: "RSASSA-PKCS1-V1_5",
hash: { name: "SHA-256" }
},
false,
["sign"]
);
const binarySignature = await crypto.subtle.sign({ name: "RSASSA-PKCS1-V1_5" }, signer, buf);
return arrayBufferToBase64Url(binarySignature);
}
const jwtHeader = objectToBase64url({ alg: "RS256", typ: "JWT" });
try {
const assertiontime = Math.round(Date.now() / 1000)
const expirytime = assertiontime + 3600
const claimset = objectToBase64url({
"iss": user,
"scope": scope,
"aud": "https://oauth2.googleapis.com/token",
"exp": expirytime,
"iat": assertiontime
})
const jwtUnsigned = jwtHeader + "." + claimset
const signedJwt = jwtUnsigned + "." + (await sign(jwtUnsigned, key))
const body = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + signedJwt;
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cache-Control": "no-cache",
"Host": "oauth2.googleapis.com"
},
body: body
});
const oauth = await response.json();
return oauth.access_token;
} catch (err) {
console.log(err)
}
}
@KeKs0r
Copy link

KeKs0r commented Jan 25, 2023

@Moumouls I tried it with calling document ai and it works from my cloudflare worker.

@tomfuertes
Copy link

Love you all tyty

@AdityaSher
Copy link

is this still functional getting a 1042 error for some reason

@Schachte
Copy link

Schachte commented Oct 7, 2023

is this still functional getting a 1042 error for some reason

@AdityaSher

Try this:https://ryan-schachte.com/blog/oauth_cloudflare_workers/

@AdityaSher
Copy link

@Schachte false flag this seems to be working again, wasnt working for a brief while because of the cloudflare --remote service went down.

@gaieges
Copy link

gaieges commented Mar 14, 2025

Amazing folks, thanks for putting this together. Saved my butt!

@nwparker
Copy link

Not sure why none of the other code in the thread works, here's something that works for me -- a modification of everything above.

Just call:
getGoogleAuthToken({private_key: 'SET_THIS_SECRET', client_email: 'SET_THIS_SECRET'}, ['ADD-YOUR-SCOPES'])
Using scopes: https://developers.google.com/identity/protocols/oauth2/scopes

// Inspiration: https://gist.github.com/markelliot/6627143be1fc8209c9662c504d0ff205
const { subtle } = globalThis.crypto;

const PEM_HEADER = '-----BEGIN PRIVATE KEY-----';
const PEM_FOOTER = '-----END PRIVATE KEY-----';

function objectToBase64url(object: object): string {
  return arrayBufferToBase64Url(new TextEncoder().encode(JSON.stringify(object)) as unknown as ArrayBuffer);
}

function arrayBufferToBase64Url(buffer: ArrayBuffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
}

function str2ab(str: string) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i += 1) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

async function sign(content: string, signingKey: string) {
  const buf = str2ab(content);
  const plainKey = signingKey
    .replace(/(\r\n|\n|\r)/gm, '')
    .replace(/\\n/g, '')
    .replace(PEM_HEADER, '')
    .replace(PEM_FOOTER, '')
    .trim();

  const binaryKey = str2ab(atob(plainKey));
  const signer = await subtle.importKey(
    'pkcs8',
    binaryKey,
    {
      name: 'RSASSA-PKCS1-V1_5',
      hash: { name: 'SHA-256' },
    },
    false,
    ['sign'],
  );
  const binarySignature = await subtle.sign({ name: 'RSASSA-PKCS1-V1_5' }, signer, buf);
  return arrayBufferToBase64Url(binarySignature);
}

export async function getGoogleAuthToken(
  credentials: {
    private_key: string;
    client_email: string;
  },
  scopes: string[],
) {
  const { client_email: user, private_key: key } = credentials;
  const scope = scopes.join(' ');
  const jwtHeader = objectToBase64url({ alg: 'RS256', typ: 'JWT' });

  try {
    const assertiontime = Math.round(Date.now() / 1000);
    const expirytime = assertiontime + 3600;
    const claimset = objectToBase64url({
      iss: user,
      scope,
      aud: 'https://oauth2.googleapis.com/token',
      exp: expirytime,
      iat: assertiontime,
    });

    const jwtUnsigned = `${jwtHeader}.${claimset}`;
    const signedJwt = `${jwtUnsigned}.${await sign(jwtUnsigned, key)}`;
    const body = `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${signedJwt}`;

    const response = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Cache-Control': 'no-cache',
        Host: 'oauth2.googleapis.com',
      },
      body,
    });

    const resp = (await response.json()) as { access_token: string };
    return resp.access_token;
  } catch (e) {
    console.error(e);
    throw e;
  }
}

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