@pedwise/next-firebase-auth-edge
Version:
Next.js 13 Firebase Authentication for Edge and server runtimes. Dedicated for Next 13 server components. Compatible with Next.js middleware.
171 lines • 6.89 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeJwt = exports.verifyJwtSignature = exports.EmulatorSignatureVerifier = exports.PublicKeySignatureVerifier = exports.NextCachedUrlKeyFetcher = exports.ALGORITHM_RS256 = void 0;
const validator_1 = require("./validator");
const error_1 = require("./jwt/error");
const jwt_1 = require("./jwt");
const verify_1 = require("./jwt/verify");
const response_cache_1 = require("./response-cache");
exports.ALGORITHM_RS256 = "RS256";
const NO_MATCHING_KID_ERROR_MESSAGE = "no-matching-kid-error";
const NO_KID_IN_HEADER_ERROR_MESSAGE = "no-kid-in-header-error";
function getExpiresAt(res) {
if (!res.headers.has("cache-control")) {
return 0;
}
const cacheControlHeader = res.headers.get("cache-control");
const parts = cacheControlHeader.split(",");
const maxAge = parts.reduce((acc, part) => {
const subParts = part.trim().split("=");
if (subParts[0] === "max-age") {
return +subParts[1];
}
return acc;
}, 0);
return Date.now() + maxAge * 1000;
}
class NextCachedUrlKeyFetcher {
constructor(clientCertUrl) {
this.clientCertUrl = clientCertUrl;
this.cache = (0, response_cache_1.getResponseCache)();
if (!(0, validator_1.isURL)(clientCertUrl)) {
throw new Error("The provided public client certificate URL is not a valid URL.");
}
}
async fetchPublicKeysResponse(url) {
const res = await fetch(url);
if (!res.ok) {
let errorMessage = "Error fetching public keys for Google certs: ";
const data = await res.json();
if (data.error) {
errorMessage += `${data.error}`;
if (data.error_description) {
errorMessage += " (" + data.error_description + ")";
}
}
else {
errorMessage += `${await res.text()}`;
}
throw new Error(errorMessage);
}
const clone = res.clone();
const data = await res.json();
if (data.error) {
throw new error_1.JwtError(error_1.JwtErrorCode.KEY_FETCH_ERROR, data.error);
}
const publicKeys = data;
const body = JSON.stringify({
publicKeys,
publicKeysExpireAt: getExpiresAt(res),
});
return new Response(body, clone);
}
async fetchAndCachePublicKeys(url) {
const res = await this.fetchPublicKeysResponse(url);
await this.cache.put(url, res.clone());
return (await res.json()).publicKeys;
}
async fetchPublicKeys() {
const url = new URL(this.clientCertUrl);
const cachedResponse = await this.cache.get(url);
if (!cachedResponse) {
return this.fetchAndCachePublicKeys(url);
}
const { publicKeys, publicKeysExpireAt } = await cachedResponse.json();
if (publicKeysExpireAt <= Date.now()) {
return this.fetchAndCachePublicKeys(url);
}
return publicKeys;
}
}
exports.NextCachedUrlKeyFetcher = NextCachedUrlKeyFetcher;
class PublicKeySignatureVerifier {
constructor(keyFetcher) {
this.keyFetcher = keyFetcher;
if (!(0, validator_1.isNonNullObject)(keyFetcher)) {
throw new Error("The provided key fetcher is not an object or null.");
}
}
static withCertificateUrl(clientCertUrl) {
return new PublicKeySignatureVerifier(new NextCachedUrlKeyFetcher(clientCertUrl));
}
async verify(token) {
if (!(0, validator_1.isString)(token)) {
return Promise.reject(new error_1.JwtError(error_1.JwtErrorCode.INVALID_ARGUMENT, "The provided token must be a string."));
}
const decoded = (0, jwt_1.decode)(token, { complete: true });
const publicKey = await getKey(this.keyFetcher, decoded.header);
return verifyJwtSignature(token, publicKey).catch((error) => {
if (error.code === error_1.JwtErrorCode.NO_KID_IN_HEADER) {
return this.verifyWithoutKid(token);
}
throw error;
});
}
verifyWithoutKid(token) {
return this.keyFetcher
.fetchPublicKeys()
.then((publicKeys) => this.verifyWithAllKeys(token, publicKeys));
}
verifyWithAllKeys(token, keys) {
const promises = [];
Object.values(keys).forEach((key) => {
const result = verifyJwtSignature(token, key)
.then(() => true)
.catch((error) => {
if (error.code === error_1.JwtErrorCode.TOKEN_EXPIRED) {
throw error;
}
return false;
});
promises.push(result);
});
return Promise.all(promises).then((result) => {
if (result.every((r) => r === false)) {
throw new error_1.JwtError(error_1.JwtErrorCode.INVALID_SIGNATURE, "Invalid token signature.");
}
});
}
}
exports.PublicKeySignatureVerifier = PublicKeySignatureVerifier;
class EmulatorSignatureVerifier {
verify(token) {
// Signature checks skipped for emulator; no need to fetch public keys.
return verifyJwtSignature(token, "");
}
}
exports.EmulatorSignatureVerifier = EmulatorSignatureVerifier;
async function getKey(fetcher, header) {
if (!header.kid) {
throw new Error(NO_KID_IN_HEADER_ERROR_MESSAGE);
}
const kid = header.kid || "";
const publicKeys = await fetcher.fetchPublicKeys();
if (!Object.prototype.hasOwnProperty.call(publicKeys, kid)) {
throw new Error(NO_MATCHING_KID_ERROR_MESSAGE);
}
return publicKeys[kid];
}
async function verifyJwtSignature(token, secretOrPublicKey) {
if (!token) {
throw new error_1.JwtError(error_1.JwtErrorCode.INVALID_ARGUMENT, "The provided token must be a string.");
}
await (0, verify_1.verify)(token, secretOrPublicKey);
}
exports.verifyJwtSignature = verifyJwtSignature;
function decodeJwt(jwtToken) {
if (!(0, validator_1.isString)(jwtToken)) {
return Promise.reject(new error_1.JwtError(error_1.JwtErrorCode.INVALID_ARGUMENT, "The provided token must be a string."));
}
const fullDecodedToken = (0, jwt_1.decode)(jwtToken, {
complete: true,
});
if (!fullDecodedToken) {
return Promise.reject(new error_1.JwtError(error_1.JwtErrorCode.INVALID_ARGUMENT, "Decoding token failed."));
}
const header = fullDecodedToken === null || fullDecodedToken === void 0 ? void 0 : fullDecodedToken.header;
const payload = fullDecodedToken === null || fullDecodedToken === void 0 ? void 0 : fullDecodedToken.payload;
return Promise.resolve({ header, payload });
}
exports.decodeJwt = decodeJwt;
//# sourceMappingURL=signature-verifier.js.map