Skip to content

Instantly share code, notes, and snippets.

@markelliot
Last active January 24, 2024 22:19
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • 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)
}
}
@yigit-serin
Copy link

Hello,
Thank you for the great code.
When I want to use it, can not get the token.
At line 68 sign() method needs an await .
After that I get the token.

@hanayashiki
Copy link

hanayashiki commented Jan 18, 2022

TypeScript version

/**
 * 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
 */
export async function getGoogleAuthToken(user: string, key: string, scope: string): Promise<string> {
  function objectToBase64url(object: object) {
    return arrayBufferToBase64Url(
      new TextEncoder().encode(JSON.stringify(object)),
    )
  }
  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++) {
      bufView[i] = str.charCodeAt(i);
    }
    return buf;
  };
  async function sign(content: string, signingKey: string) {
    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 + "." + 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)
  }
}

@Moumouls
Copy link

A refactored version with utils functions outside of the main function, shorthands, template string

const objectToBase64url = (object: object) =>
	arrayBufferToBase64Url(new TextEncoder().encode(JSON.stringify(object)))

const arrayBufferToBase64Url = (buffer: ArrayBuffer) =>
	btoa(String.fromCharCode(...new Uint8Array(buffer)))
		.replace(/=/g, '')
		.replace(/\+/g, '-')
		.replace(/\//g, '_')

const 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
}

const sign = async (content: string, signingKey: string) => {
	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 getGoogleAuthToken = async (
	user: string,
	key: string,
	scope: string,
): Promise<string> => {
	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}.${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 { access_token } = await response.json()
		return access_token
	} catch (err) {
		console.error(err)
	}
}

@Matt-B50
Copy link

This is great - the original works perfectly for me, thank you!

@Schachte
Copy link

Schachte commented Oct 24, 2022

Pushed as module as this was a bit of a pain for fun personal projects.
Supports Typescript.

npm install cloudflare-workers-and-google-oauth

import GoogleAuth, { GoogleKey } from 'cloudflare-workers-and-google-oauth'

// Add secret using Wranlger or the Cloudflare dash
export interface Env {
	GCP_SERVICE_ACCOUNT: string;
}

export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext
	): Promise<Response> {
		const scopes: string[] = ['https://www.googleapis.com/auth/devstorage.full_control']
		const googleAuth: GoogleKey = JSON.parse(env.GCP_SERVICE_ACCOUNT)

		const oauth = new GoogleAuth(googleAuth, scopes)
		const token = await oauth.getGoogleAuthToken()

                 // Example with Google Cloud Storage
		const res = await fetch('https://storage.googleapis.com/storage/v1/b/MY_BUCKET/o/MY_OBJECT.png?alt=media', {
			method: 'GET',
			headers: {
				'Authorization': `Bearer ${token}`,
				'Content-Type': 'image/png',
				'Accept': 'image/png',
			},
		})

		return new Response(res.body, { headers: { 'Content-Type': 'image/png' } });
	},
};

@KeKs0r
Copy link

KeKs0r commented Jan 24, 2023

Since I came to this gist trying to solve the same problem. I found a solution that works with self signing and therefore doe snot add an additional request.

It might not work with all google apis, but I tested it with pubsub and document ai

Source: https://gist.github.com/KeKs0r/92be7af08d1d10eae8d1328c78de5f07

@Moumouls
Copy link

Interesting @KeKs0r any chance to give it a little try on your side by spinning up an hello world container on cloud run ?

@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/cf-workers-auth

@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.

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