-
-
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
@Schachte false flag this seems to be working again, wasnt working for a brief while because of the cloudflare --remote service went down.
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.