Skip to content

Google Login GSI Support #17

@Code6226

Description

@Code6226

I was hoping your library would support verifying a Google Login token as described here https://developers.google.com/identity/gsi/web/guides/verify-google-id-token

It was looking a different endpoint to verify Google's keys, and it was expecting a different audience.

I've modified your code and pieced it together to do just what I need. Feel free to incorporate these changes in your library in a way that doesn't break your other use cases:

import { decodeProtectedHeader, jwtVerify } from "jose";
import { importX509 } from "jose";
const inFlight = new Map();
const cache = new Map();

const canUseDefaultCache =
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  typeof globalThis.caches?.default?.put === "function";

/**
 * Imports a public key for the provided Google Cloud (GCP)
 * service account credentials.
 *
 * @throws {FetchError} - If the X.509 certificate could not be fetched.
 */
async function importPublicKey(options) {
  const keyId = options.keyId;
  const certificateURL = options.certificateURL ?? "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; // prettier-ignore
  const cacheKey = `${certificateURL}?key=${keyId}`;
  const value = cache.get(cacheKey);
  const now = Date.now();
  async function fetchKey() {
    // Fetch the public key from Google's servers
    const res = await fetch(certificateURL);
    if (!res.ok) {
      const error = await res
        .json()
        .then((data) => data.error.message)
        .catch(() => undefined);
      throw new FetchError(error ?? "Failed to fetch the public key", {
        response: res,
      });
    }
    const data = await res.json();
    const x509 = data[keyId];
    if (!x509) {
      throw new Error(`Public key "${keyId}" not found.`);
    }
    const key = await importX509(x509, "RS256");
    // Resolve the expiration time of the key
    const maxAge = res.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1]; // prettier-ignore
    const expires = Date.now() + Number(maxAge ?? "3600") * 1000;
    // Update the local cache
    cache.set(cacheKey, { key, expires });
    inFlight.delete(keyId);
    return key;
  }
  // Attempt to read the key from the local cache
  if (value) {
    if (value.expires > now + 10_000) {
      // If the key is about to expire, start a new request in the background
      if (value.expires - now < 600_000) {
        const promise = fetchKey();
        inFlight.set(cacheKey, promise);
        if (options.waitUntil) {
          options.waitUntil(promise);
        }
      }
      return value.key;
    }
    else {
      cache.delete(cacheKey);
    }
  }
  // Check if there is an in-flight request for the same key ID
  let promise = inFlight.get(cacheKey);
  // If not, start a new request
  if (!promise) {
    promise = fetchKey();
    inFlight.set(cacheKey, promise);
  }
  return await promise;
}

// based on https://www.npmjs.com/package/web-auth-library?activeTab=code
// made to check per Google's recommendations: https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
export async function verifyIdToken(options) {
  if (!options?.idToken) {
    throw new TypeError(`Missing "idToken"`);
  }
  let clientId = options?.clientId;
  if (clientId === undefined) {
    throw new TypeError(`Missing "clientId"`);
  }
  if (!options.waitUntil && canUseDefaultCache) {
    console.warn("Missing `waitUntil` option.")
  }
  // Import the public key from the Google Cloud project
  const header = decodeProtectedHeader(options.idToken);
  const now = Math.floor(Date.now() / 1000);
  const key = await importPublicKey({
    keyId: header.kid,
    certificateURL: "https://www.googleapis.com/oauth2/v1/certs",
    waitUntil: options.waitUntil,
  });
  const { payload } = await jwtVerify(options.idToken, key, {
    audience: clientId,
    issuer: ['https://accounts.google.com','accounts.google.com'],
    maxTokenAge: "1h",
    clockTolerance: '5m'
  });
  if (!payload.sub) {
    throw new Error(`Missing "sub" claim`);
  }
  if (typeof payload.auth_time === "number" && payload.auth_time > now) {
    throw new Error(`Unexpected "auth_time" claim value`);
  }
  return payload;
}

Used like so:

let decoded = await verifyIdToken({
	idToken: googleLoginToken,
	clientId: env.GOOGLE_CLIENT_ID,
	waitUntil: context.waitUntil
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions