Skip to content

Instantly share code, notes, and snippets.

@markelliot
Last active June 19, 2024 16:40
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)
}
}
@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