Using Typescript to generate a self-signed JWT token for use on GCP

Stuart McLean
3 min readApr 2, 2023

--

Make it easy on yourself and still keep things secure.

Casino tokens scattered on a gaming table.
Photo by Pavel Danilyuk: https://www.pexels.com/photo/casino-chips-scattered-on-gaming-table-7594225/

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

Key on a person’s palm.
Photo by Kindel Media: https://www.pexels.com/photo/key-on-a-person-s-palm-7579201/

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! 😸

--

--

Stuart McLean

I like helping people to discover their own potential. He/him. Full-time parent & software developer, part-time teacher & musician.