Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Login GSI Support #17

Open
Code6226 opened this issue Mar 30, 2023 · 0 comments
Open

Google Login GSI Support #17

Code6226 opened this issue Mar 30, 2023 · 0 comments

Comments

@Code6226
Copy link

Code6226 commented Mar 30, 2023

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
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant