Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Created August 30, 2024 00:02
Show Gist options
  • Save DinoChiesa/b896f7f8b89119bae5e1b6cc111a5464 to your computer and use it in GitHub Desktop.
Save DinoChiesa/b896f7f8b89119bae5e1b6cc111a5464 to your computer and use it in GitHub Desktop.
Get GCP ID Token for a Service Account, using a JSON-encoded Service Account key
$ node ./getIdTokenWithServiceAccount.js --keyfile ./my-service-account-key-a8ef19f432a9.json --audience https://foo-bar/bam
jwt payload: {
"iss": "[email protected]",
"aud": "https://oauth2.googleapis.com/token",
"iat": 1724976008,
"exp": 1724976068,
"target_audience": "https://foo-bar/bam"
}
assertion: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzaGVldC13cml0ZXItMUBkY2hpZXNhLWFyZ29saXMtMi5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsImF1ZCI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiaWF0IjoxNzI0OTc2MDA4LCJleHAiOjE3MjQ5NzYwNjgsInRhcmdldF9hdWRpZW5jZSI6Imh0dHBzOi8vZm9vLWJhci9iYW0ifQ.rxVh1-rpXxn67zh94LBDLJm3j2jDqzlXXxV9AqUVtVYDVoKvLy5PH7oBFxrO9RgnhvYkxmbYhMWC5bKmAsaB1J7Y7m3Ch7N2C05kzvle8RHImMsIdW7_nLEISKYgZLmUTQh_oqqgyysmY6C6q0Hadt7yqJ7rZz1W_-wq2fV0hZVTAZLKlUtXefKrwK90Myzo3yZg5tA7GTFUY23b8D4gSEkMxjGR0Ke3PwR4N9SK4FKy8YlYeDsOUGfX2GNmqEIQpD7AfjLBUbnFJeKVL04c8PKXnvUAiffeqfCkwcVpVNoyFexEsB2e9ZUUL2H4A7tRR4cA0DU3OL0dkm3Bhd9qHQ
token response:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImE0OTM5MWJmNTJiNThjMWQ1NjAyNTVjMmYyYTA0ZTU5ZTIyYTdiNjUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2Zvby1iYXIvYmFtIiwiYXpwIjoic2hlZXQtd3JpdGVyLTFAZGNoaWVzYS1hcmdvbGlzLTIuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InNoZWV0LXdyaXRlci0xQGRjaGllc2EtYXJnb2xpcy0yLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTcyNDk3OTYwOCwiaWF0IjoxNzI0OTc2MDA4LCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMDk0MjUzOTg3OTkzMDczMjU0NjIifQ.e4yythPb-imAsnsYDUxhrAX_aqDrBSQ1mtkyREEVtsWqpce7UlgEIbfSFS-LQUNVJAp89_HNY4f9NjLmB4mtax335TXipWEjc4ofxn0JPFGU8DwPjSWeb_39JQzLzMevAqzdYxK6tvGRmLU-D6S6NkbtWDn2Wun1ZKTh6RlzAFnPntx0AKtcECcb6vTvBCbVvgoIaJoFcA8CGIKZOr_dCqHsXTlHjggwCkLivyMfQPeVm7dI9EKgw0qFf9IPG0yyVlCfB3T4JSNRuphiifH9nDK1AF1txmjptAOkzFaR7tTwFesvzN3Y54OE0Y4TiI2jVni3xxxW0-Xe9loeIMgXrA"
}
token info:
{
"aud": "https://foo-bar/bam",
"azp": "[email protected]",
"email": "[email protected]",
"email_verified": "true",
"exp": "1724979608",
"iat": "1724976008",
"iss": "https://accounts.google.com",
"sub": "109425398799307325462",
"alg": "RS256",
"kid": "a49391bf52b58c1d560255c2f2a04e59e22a7b65",
"typ": "JWT"
}
// getIdTokenWithServiceAccount.js
// ------------------------------------------------------------------
//
// Copyright 2019-2024 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// uses only modules that are builtin to node, no external dependencies.
//
/* jshint esversion:9, node:true, strict:implied */
/* global process, console, Buffer */
const crypto = require("crypto"),
util = require("util"),
fs = require("fs"),
path = require("path");
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
function logWrite() {
console.log(util.format.apply(null, arguments) + "\n");
}
const toBase64UrlNoPadding = (s) =>
s.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
const base64EncodeString = (theString) =>
toBase64UrlNoPadding(Buffer.from(theString).toString("base64"));
function signJwt(header, payload, key) {
if (!header.alg) {
throw new Error("missing alg");
}
if (header.alg != "RS256") {
throw new Error("unhandled alg: " + header.alg);
}
const signer = crypto.createSign("sha256");
const signatureBase = [header, payload]
.map((x) => base64EncodeString(JSON.stringify(x)))
.join(".");
signer.update(signatureBase);
const computedSignature = toBase64UrlNoPadding(signer.sign(key, "base64"));
return signatureBase + "." + computedSignature;
}
function getGoogleAuthJwt({ options }) {
const keyfile = options.keyfile,
nowInSeconds = Math.floor(Date.now() / 1000),
jwtHeader = { alg: "RS256", typ: "JWT" },
jwtClaims = {
iss: keyfile.client_email,
aud: keyfile.token_uri,
iat: nowInSeconds,
exp: nowInSeconds + 60,
target_audience: options.audience
};
logWrite("jwt payload: " + JSON.stringify(jwtClaims, null, 2));
return Promise.resolve({
options,
assertion: signJwt(jwtHeader, jwtClaims, keyfile.private_key)
});
}
function redeemJwtForGcpToken(ctx) {
logWrite("assertion: " + util.format(ctx.assertion));
const url = ctx.options.keyfile.token_uri,
headers = {
"content-type": "application/x-www-form-urlencoded"
},
method = "post",
body = `grant_type=${GRANT_TYPE}&assertion=${ctx.assertion}`;
return fetch(url, { method, headers, body }).then(
async (response) => await response.json()
);
}
function processArgs(args) {
let awaiting = null;
const options = {};
try {
args.forEach((arg) => {
if (awaiting) {
if (awaiting == "--keyfile") {
options.keyfile = arg;
awaiting = null;
} else if (awaiting == "--audience") {
options.audience = arg;
awaiting = null;
} else {
throw new Error(`I'm confused: ${arg}`);
}
} else {
switch (arg) {
case "--keyfile":
if (options.keyfile) {
throw new Error("duplicate argument: " + arg);
}
awaiting = arg;
break;
case "--audience":
if (options.audience) {
throw new Error("duplicate argument: " + arg);
}
awaiting = arg;
break;
case "-h":
case "--help":
return;
default:
throw new Error("unexpected argument: " + arg);
}
}
});
return options;
} catch (exc1) {
console.log("Exception:" + util.format(exc1));
return;
}
}
function usage() {
const basename = path.basename(process.argv[1]);
console.log(
`usage:\n node ${basename} --keyfile SERVICE_ACCOUNT_KEYFILE --audience DESIRED_AUDIENCE\n`
);
}
async function showTokenInfo(payload) {
const response = await fetch(
`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${payload.id_token}`,
{
method: "get",
body: null,
headers: {}
}
),
body = await response.json();
console.log("\ntoken info:\n" + JSON.stringify(body, null, 2));
}
function main(args) {
try {
const options = processArgs(args);
if (options && options.keyfile && options.audience) {
options.keyfile = JSON.parse(fs.readFileSync(options.keyfile, "utf8"));
if (!options.keyfile.client_email || !options.keyfile.token_uri) {
throw new Error("that does not look like a Service Account key file.");
}
getGoogleAuthJwt({ options })
.then(redeemJwtForGcpToken)
.then(
(payload) => (
console.log("token response:\n" + JSON.stringify(payload, null, 2)),
payload
)
)
.then((payload) => showTokenInfo(payload))
.catch((e) => console.log(util.format(e)));
} else {
usage();
}
} catch (e) {
console.log("Exception:" + util.format(e));
}
}
main(process.argv.slice(2));
{
"name": "get-gcp-id-token-with-service-account",
"version": "1.0.1",
"description": "get a GCP ID Token, using a JSON-encoded Service Account key",
"main": "getIdTokenWithServiceAccount.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "[email protected]",
"license": "Apache-2.0",
"dependencies": {
},
"engines": {
"node": ">=18.0.0"
}
}
@DinoChiesa
Copy link
Author

See this repo for a more complete treatment
https://github.com/DinoChiesa/get-gcp-id-token

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