UNPKG

web-auth-library

Version:

Authentication library for the browser environment using Web Crypto API

130 lines (129 loc) 5.19 kB
/* SPDX-FileCopyrightText: 2022-present Kriasoft */ /* SPDX-License-Identifier: MIT */ import { decodeJwt } from "jose"; import { canUseDefaultCache } from "../core/env.js"; import { FetchError } from "../core/error.js"; import { logOnce } from "../core/utils.js"; import { getCredentials } from "./credentials.js"; import { createCustomToken } from "./customToken.js"; const defaultCache = new Map(); /** * Fetches an access token from Google Cloud API using the provided * service account credentials. * * @throws {FetchError} — If the access token could not be fetched. */ export async function getAccessToken(options) { if (!options?.waitUntil && canUseDefaultCache) { logOnce("warn", "verifyIdToken", "Missing `waitUntil` option."); } let credentials; // Normalize service account credentials // using env.GOOGLE_CLOUD_CREDENTIALS as a fallback if (options?.credentials) { credentials = getCredentials(options.credentials); } else { if (!options?.env?.GOOGLE_CLOUD_CREDENTIALS) { throw new TypeError("Missing credentials"); } credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); } // Normalize authentication scope and audience values const scope = Array.isArray(options.scope) ? options.scope.join(",") : options.scope; const audience = Array.isArray(options.audience) ? options.audience.join(",") : options.audience; const tokenUrl = credentials.token_uri; // Create a cache key that can be used with Cloudflare Cache API const cacheKeyUrl = new URL(tokenUrl); cacheKeyUrl.searchParams.set("scope", scope ?? ""); cacheKeyUrl.searchParams.set("aud", audience ?? ""); cacheKeyUrl.searchParams.set("key", credentials.private_key_id); const cacheKey = cacheKeyUrl.toString(); // Attempt to retrieve the token from the cache const cache = options.cache ?? defaultCache; const cacheValue = cache.get(cacheKey); let now = Math.floor(Date.now() / 1000); if (cacheValue) { if (cacheValue.created > now - 60 * 60) { let token = await cacheValue.promise; if (token.expires > now) { return token.token; } else { const nextValue = cache.get(cacheKey); if (nextValue && nextValue !== cacheValue) { token = await nextValue.promise; if (token.expires > now) { return token.token; } else { cache.delete(cacheKey); } } } } else { cache.delete(cacheKey); } } const promise = (async () => { let res; // Attempt to retrieve the token from Cloudflare cache // if the code is running in Cloudflare Workers environment if (canUseDefaultCache) { res = await caches.default.match(cacheKey); } if (!res) { now = Math.floor(Date.now() / 1000); // Request a new token from the Google Cloud API const jwt = await createCustomToken({ credentials, scope: options.audience ?? options.scope, }); const body = new URLSearchParams(); body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); body.append("assertion", jwt); res = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); if (!res.ok) { const error = await res .json() .then((data) => data?.error_description) .catch(() => undefined); throw new FetchError(error ?? "Failed to fetch an access token.", { response: res, }); } if (canUseDefaultCache) { let cacheRes = res.clone(); cacheRes = new Response(cacheRes.body, cacheRes); cacheRes.headers.set("Cache-Control", `max-age=3590, public`); cacheRes.headers.set("Last-Modified", new Date().toUTCString()); const cachePromise = caches.default.put(cacheKey, cacheRes); if (options.waitUntil) { options.waitUntil(cachePromise); } } } const data = await res.json(); if ("id_token" in data) { const claims = decodeJwt(data.id_token); return { token: data.id_token, expires: claims.exp }; } const lastModified = res.headers.get("last-modified"); const expires = lastModified ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in : now + data.expires_in; return { expires, token: data.access_token }; })(); cache.set(cacheKey, { created: now, promise }); return await promise.then((data) => data.token); } // #endregion