Skip to content

Instantly share code, notes, and snippets.

@nyamadan
Created January 21, 2024 08:59
Show Gist options
  • Select an option

  • Save nyamadan/1c39a1b1bfdf80b5083b3ec070fcdd5c to your computer and use it in GitHub Desktop.

Select an option

Save nyamadan/1c39a1b1bfdf80b5083b3ec070fcdd5c to your computer and use it in GitHub Desktop.

Revisions

  1. nyamadan created this gist Jan 21, 2024.
    110 changes: 110 additions & 0 deletions id-token.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,110 @@
    // https://cloud.google.com/run/docs/authenticating/service-to-service

    export interface IdTokenError {
    error: string;
    error_description: string;
    }

    export interface IdTokenSuccess extends IdToken {
    error?: never;
    }

    export interface IdToken {
    id_token: string;
    }

    export interface AccountCredential {
    client_email: string;
    private_key: string;
    }

    function arrayBufferToBase64(buffer: ArrayBuffer) {
    let binary = "";
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
    }

    function arrayBufferToBase64Url(buffer: ArrayBuffer) {
    const base64 = arrayBufferToBase64(buffer);
    return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
    }

    function base64ToArrayBuffer(base64: string) {
    while (base64.length % 4) {
    base64 += "=";
    }
    const binary = atob(base64);
    const length = binary.length;
    const buffer = new ArrayBuffer(length);
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < length; i++) {
    bytes[i] = binary.charCodeAt(i);
    }
    return buffer;
    }

    export function base64UrlToArrayBuffer(base64url: string) {
    const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
    return base64ToArrayBuffer(base64);
    }

    export async function getIdToken(
    target_audience: string,
    credential: AccountCredential,
    ): Promise<IdTokenSuccess | IdTokenError> {
    const privateKeyDER = base64ToArrayBuffer(
    credential.private_key.trim().split("\n").slice(1, -1).join(""),
    );

    const algorithm = {
    name: "RSASSA-PKCS1-v1_5",
    hash: { name: "SHA-256" },
    } as const;

    const privateKey = await crypto.subtle.importKey(
    "pkcs8",
    privateKeyDER,
    algorithm,
    false,
    ["sign"],
    );

    const header = { alg: "RS256", typ: "JWT" } as const;
    const clientEmail = credential.client_email;
    const aud = "https://www.googleapis.com/oauth2/v4/token";
    const iss = clientEmail;
    const iat = (Date.now() / 1000) | 0;
    const exp = iat + 3600;
    const sub = clientEmail;
    const payload = { target_audience, aud, iss, sub, iat, exp } as const;
    const encoder = new TextEncoder();
    const encodedHeader = arrayBufferToBase64Url(
    encoder.encode(JSON.stringify(header)),
    );
    const encodedPayload = arrayBufferToBase64Url(
    encoder.encode(JSON.stringify(payload)),
    );
    const encodedMessage = `${encodedHeader}.${encodedPayload}`;
    const encodedMessageArrBuf = encoder.encode(encodedMessage);
    const signatureArrBuf = await crypto.subtle.sign(
    algorithm,
    privateKey,
    encodedMessageArrBuf,
    );
    const encodedSignature = arrayBufferToBase64Url(signatureArrBuf);
    const jwtToken = `${encodedMessage}.${encodedSignature}`;
    const response = await fetch("https://www.googleapis.com/oauth2/v4/token", {
    method: "POST",
    headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
    grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
    assertion: jwtToken,
    }),
    });
    return await response.json();
    }