-
-
Save markelliot/6627143be1fc8209c9662c504d0ff205 to your computer and use it in GitHub Desktop.
/** | |
* 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) | |
} | |
} |
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)
}
}
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)
}
}
This is great - the original works perfectly for me, thank you!
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' } });
},
};
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
Interesting @KeKs0r any chance to give it a little try on your side by spinning up an hello world container on cloud run ?
@Moumouls I tried it with calling document ai and it works from my cloudflare worker.
Love you all tyty
is this still functional getting a 1042 error for some reason
is this still functional getting a 1042 error for some reason
Try this:https://ryan-schachte.com/blog/oauth_cloudflare_workers/
@Schachte false flag this seems to be working again, wasnt working for a brief while because of the cloudflare --remote service went down.
Amazing folks, thanks for putting this together. Saved my butt!
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;
}
}
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.