Using Typescript to generate a self-signed JWT token for use on GCP
Make it easy on yourself and still keep things secure.
For an external application to access the Google Cloud Platform (GCP) it needs a service account and a way to authenticate itself.
GCP offers several ways to do this, including OAuth 2.0 and OpenID Connect (OIDC). In this article, we’ll examine the self-signed JWT method.
It’s useful to allow external services like Cloudflare Workers access to GCP content. Since the process for generating a JWT is rather complex and, for now at least, Cloudflare only allows workers to be written in JavaScript, I thought it best to write in TypeScript.
The Request Payload
First, we need to create a payload in the format that GCP expects. This requires a service account (which looks like an email address) and a target audience, both of which are included in a GCP service account key. We can also specify a time to live (TTL) for the token, measured in seconds.
function currentTimeInSeconds(): number {
return Math.floor(Date.now() / 1000);
}
class Payload {
iss: string;
sub: string;
email: string;
aud: string;
jti: string;
target_audience: string;
exp: number;
iat: number;
constructor(serviceAcct: string, targetAudience: string, ttlSeconds: number) {
const time = currentTimeInSeconds();
this.iss = serviceAcct;
this.sub = serviceAcct;
this.email = serviceAcct;
this.aud = "https://www.googleapis.com/oauth2/v4/token";
this.exp = time + ttlSeconds;
this.jti = "jwt_nonce";
this.iat = time;
this.target_audience = targetAudience;
}
}
Signing our Token
As outlined in the spec at jwt.io, signatures an algorithm like this:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
To help us, we can use the jsrassign library to help us sign our token, again
const header = {
alg: "RS256",
typ: "JWT",
kid: serviceAcct,
};
const payload = new Payload(serviceAcct, targetAudience, ttlSeconds);
const jwt = KJUR.jws.JWS.sign(
header.alg,
JSON.stringify(header),
JSON.stringify(payload),
privateKey
);
Fetching the Token
GCP requires the body of the JWT request in the following format:
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + jwt;
We can then include this in a request formatted:
const newRequestInit = {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + jwt,
};
This will allow us to create a token using an existing Request
like this:
const url = new URL("https://www.googleapis.com/oauth2/v4/token");
const newRequest = new Request(request, newRequestInit);
const tokenRequest = new Request(url, newRequest);
…and actually fetch the token like this:
const token = await fetch(tokenRequest)
.then(function (response: Response) {
return JSON.parse(response.text()).id_token;
});
}
This token can then be used in the Authorization
header of your request.
Forwarding Existing Authorization Tokens
Since GCP requires the Authorization
header, you will need to forward any existing authorization headers to one of your downstream services with a different key.
And you should definitely use authorization in your downstream services.
I recommend moving the header to something like x-authorization-external
and accessing that as your bearer token in the service.
if ("Authorization" in request.headers) {
headers["x-authorization-external"] = request.headers.get(
"Authorization"
) as string;
}
A Word of Warning
Be careful about making it too easy for other people to trigger your functions. If your Cloudflare worker or GCP cloud functions run, you will be charged for them. However, as of this writing, GCP charges are based on both the number of executions and compute time, so authorize all calls from external sources and fail early if they don’t have correct credentials.
Putting it all together
This gist shows the full TokenProvider
class along with its dependencies.
It can be called like this:
const tokenProvider = new TokenProvider(serviceAcct, endpoint, privateKey);
const jwt = tokenProvider.getIdToken();
I also added a private isExpired()
method to ensure that we can call the getIdToken()
method multiple times on a Token
object and always be assured of an unexpired token.
I hope this makes it easier to generate your tokens and trigger your GCP services securely, but with ease.
Happy coding everybody! 😸