firebase-admin
Version:
Firebase admin SDK for Node.js
356 lines (355 loc) • 14.7 kB
JavaScript
/*! firebase-admin v10.0.0 */
;
/*!
* Copyright 2021 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.JwtErrorCode = exports.JwtError = exports.decodeJwt = exports.verifyJwtSignature = exports.EmulatorSignatureVerifier = exports.PublicKeySignatureVerifier = exports.UrlKeyFetcher = exports.JwksFetcher = exports.ALGORITHM_RS256 = void 0;
var validator = require("./validator");
var jwt = require("jsonwebtoken");
var jwks = require("jwks-rsa");
var api_request_1 = require("../utils/api-request");
exports.ALGORITHM_RS256 = 'RS256';
// `jsonwebtoken` converts errors from the `getKey` callback to its own `JsonWebTokenError` type
// and prefixes the error message with the following. Use the prefix to identify errors thrown
// from the key provider callback.
// https://github.com/auth0/node-jsonwebtoken/blob/d71e383862fc735991fd2e759181480f066bf138/verify.js#L96
var JWT_CALLBACK_ERROR_PREFIX = 'error in secret or public key callback: ';
var NO_MATCHING_KID_ERROR_MESSAGE = 'no-matching-kid-error';
var NO_KID_IN_HEADER_ERROR_MESSAGE = 'no-kid-in-header-error';
var HOUR_IN_SECONDS = 3600;
var JwksFetcher = /** @class */ (function () {
function JwksFetcher(jwksUrl) {
this.publicKeysExpireAt = 0;
if (!validator.isURL(jwksUrl)) {
throw new Error('The provided JWKS URL is not a valid URL.');
}
this.client = jwks({
jwksUri: jwksUrl,
cache: false,
});
}
JwksFetcher.prototype.fetchPublicKeys = function () {
if (this.shouldRefresh()) {
return this.refresh();
}
return Promise.resolve(this.publicKeys);
};
JwksFetcher.prototype.shouldRefresh = function () {
return !this.publicKeys || this.publicKeysExpireAt <= Date.now();
};
JwksFetcher.prototype.refresh = function () {
var _this = this;
return this.client.getSigningKeys()
.then(function (signingKeys) {
// reset expire at from previous set of keys.
_this.publicKeysExpireAt = 0;
var newKeys = signingKeys.reduce(function (map, signingKey) {
map[signingKey.kid] = signingKey.getPublicKey();
return map;
}, {});
_this.publicKeysExpireAt = Date.now() + (HOUR_IN_SECONDS * 6 * 1000);
_this.publicKeys = newKeys;
return newKeys;
}).catch(function (err) {
throw new Error("Error fetching Json Web Keys: " + err.message);
});
};
return JwksFetcher;
}());
exports.JwksFetcher = JwksFetcher;
/**
* Class to fetch public keys from a client certificates URL.
*/
var UrlKeyFetcher = /** @class */ (function () {
function UrlKeyFetcher(clientCertUrl, httpAgent) {
this.clientCertUrl = clientCertUrl;
this.httpAgent = httpAgent;
this.publicKeysExpireAt = 0;
if (!validator.isURL(clientCertUrl)) {
throw new Error('The provided public client certificate URL is not a valid URL.');
}
}
/**
* Fetches the public keys for the Google certs.
*
* @returns A promise fulfilled with public keys for the Google certs.
*/
UrlKeyFetcher.prototype.fetchPublicKeys = function () {
if (this.shouldRefresh()) {
return this.refresh();
}
return Promise.resolve(this.publicKeys);
};
/**
* Checks if the cached public keys need to be refreshed.
*
* @returns Whether the keys should be fetched from the client certs url or not.
*/
UrlKeyFetcher.prototype.shouldRefresh = function () {
return !this.publicKeys || this.publicKeysExpireAt <= Date.now();
};
UrlKeyFetcher.prototype.refresh = function () {
var _this = this;
var client = new api_request_1.HttpClient();
var request = {
method: 'GET',
url: this.clientCertUrl,
httpAgent: this.httpAgent,
};
return client.send(request).then(function (resp) {
if (!resp.isJson() || resp.data.error) {
// Treat all non-json messages and messages with an 'error' field as
// error responses.
throw new api_request_1.HttpError(resp);
}
// reset expire at from previous set of keys.
_this.publicKeysExpireAt = 0;
if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) {
var cacheControlHeader = resp.headers['cache-control'];
var parts = cacheControlHeader.split(',');
parts.forEach(function (part) {
var subParts = part.trim().split('=');
if (subParts[0] === 'max-age') {
var maxAge = +subParts[1];
_this.publicKeysExpireAt = Date.now() + (maxAge * 1000);
}
});
}
_this.publicKeys = resp.data;
return resp.data;
}).catch(function (err) {
if (err instanceof api_request_1.HttpError) {
var errorMessage = 'Error fetching public keys for Google certs: ';
var resp = err.response;
if (resp.isJson() && resp.data.error) {
errorMessage += "" + resp.data.error;
if (resp.data.error_description) {
errorMessage += ' (' + resp.data.error_description + ')';
}
}
else {
errorMessage += "" + resp.text;
}
throw new Error(errorMessage);
}
throw err;
});
};
return UrlKeyFetcher;
}());
exports.UrlKeyFetcher = UrlKeyFetcher;
/**
* Class for verifying JWT signature with a public key.
*/
var PublicKeySignatureVerifier = /** @class */ (function () {
function PublicKeySignatureVerifier(keyFetcher) {
this.keyFetcher = keyFetcher;
if (!validator.isNonNullObject(keyFetcher)) {
throw new Error('The provided key fetcher is not an object or null.');
}
}
PublicKeySignatureVerifier.withCertificateUrl = function (clientCertUrl, httpAgent) {
return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl, httpAgent));
};
PublicKeySignatureVerifier.withJwksUrl = function (jwksUrl) {
return new PublicKeySignatureVerifier(new JwksFetcher(jwksUrl));
};
PublicKeySignatureVerifier.prototype.verify = function (token) {
var _this = this;
if (!validator.isString(token)) {
return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'The provided token must be a string.'));
}
return verifyJwtSignature(token, getKeyCallback(this.keyFetcher), { algorithms: [exports.ALGORITHM_RS256] })
.catch(function (error) {
if (error.code === JwtErrorCode.NO_KID_IN_HEADER) {
// No kid in JWT header. Try with all the public keys.
return _this.verifyWithoutKid(token);
}
throw error;
});
};
PublicKeySignatureVerifier.prototype.verifyWithoutKid = function (token) {
var _this = this;
return this.keyFetcher.fetchPublicKeys()
.then(function (publicKeys) { return _this.verifyWithAllKeys(token, publicKeys); });
};
PublicKeySignatureVerifier.prototype.verifyWithAllKeys = function (token, keys) {
var promises = [];
Object.values(keys).forEach(function (key) {
var result = verifyJwtSignature(token, key)
.then(function () { return true; })
.catch(function (error) {
if (error.code === JwtErrorCode.TOKEN_EXPIRED) {
throw error;
}
return false;
});
promises.push(result);
});
return Promise.all(promises)
.then(function (result) {
if (result.every(function (r) { return r === false; })) {
throw new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'Invalid token signature.');
}
});
};
return PublicKeySignatureVerifier;
}());
exports.PublicKeySignatureVerifier = PublicKeySignatureVerifier;
/**
* Class for verifying unsigned (emulator) JWTs.
*/
var EmulatorSignatureVerifier = /** @class */ (function () {
function EmulatorSignatureVerifier() {
}
EmulatorSignatureVerifier.prototype.verify = function (token) {
// Signature checks skipped for emulator; no need to fetch public keys.
return verifyJwtSignature(token, '');
};
return EmulatorSignatureVerifier;
}());
exports.EmulatorSignatureVerifier = EmulatorSignatureVerifier;
/**
* Provides a callback to fetch public keys.
*
* @param fetcher - KeyFetcher to fetch the keys from.
* @returns A callback function that can be used to get keys in `jsonwebtoken`.
*/
function getKeyCallback(fetcher) {
return function (header, callback) {
if (!header.kid) {
callback(new Error(NO_KID_IN_HEADER_ERROR_MESSAGE));
}
var kid = header.kid || '';
fetcher.fetchPublicKeys().then(function (publicKeys) {
if (!Object.prototype.hasOwnProperty.call(publicKeys, kid)) {
callback(new Error(NO_MATCHING_KID_ERROR_MESSAGE));
}
else {
callback(null, publicKeys[kid]);
}
})
.catch(function (error) {
callback(error);
});
};
}
/**
* Verifies the signature of a JWT using the provided secret or a function to fetch
* the secret or public key.
*
* @param token - The JWT to be verified.
* @param secretOrPublicKey - The secret or a function to fetch the secret or public key.
* @param options - JWT verification options.
* @returns A Promise resolving for a token with a valid signature.
*/
function verifyJwtSignature(token, secretOrPublicKey, options) {
if (!validator.isString(token)) {
return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'The provided token must be a string.'));
}
return new Promise(function (resolve, reject) {
jwt.verify(token, secretOrPublicKey, options, function (error) {
if (!error) {
return resolve();
}
if (error.name === 'TokenExpiredError') {
return reject(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'The provided token has expired. Get a fresh token from your ' +
'client app and try again.'));
}
else if (error.name === 'JsonWebTokenError') {
if (error.message && error.message.includes(JWT_CALLBACK_ERROR_PREFIX)) {
var message = error.message.split(JWT_CALLBACK_ERROR_PREFIX).pop() || 'Error fetching public keys.';
var code = JwtErrorCode.KEY_FETCH_ERROR;
if (message === NO_MATCHING_KID_ERROR_MESSAGE) {
code = JwtErrorCode.NO_MATCHING_KID;
}
else if (message === NO_KID_IN_HEADER_ERROR_MESSAGE) {
code = JwtErrorCode.NO_KID_IN_HEADER;
}
return reject(new JwtError(code, message));
}
}
return reject(new JwtError(JwtErrorCode.INVALID_SIGNATURE, error.message));
});
});
}
exports.verifyJwtSignature = verifyJwtSignature;
/**
* Decodes general purpose Firebase JWTs.
*
* @param jwtToken - JWT token to be decoded.
* @returns Decoded token containing the header and payload.
*/
function decodeJwt(jwtToken) {
if (!validator.isString(jwtToken)) {
return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'The provided token must be a string.'));
}
var fullDecodedToken = jwt.decode(jwtToken, {
complete: true,
});
if (!fullDecodedToken) {
return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'Decoding token failed.'));
}
var header = fullDecodedToken === null || fullDecodedToken === void 0 ? void 0 : fullDecodedToken.header;
var payload = fullDecodedToken === null || fullDecodedToken === void 0 ? void 0 : fullDecodedToken.payload;
return Promise.resolve({ header: header, payload: payload });
}
exports.decodeJwt = decodeJwt;
/**
* Jwt error code structure.
*
* @param code - The error code.
* @param message - The error message.
* @constructor
*/
var JwtError = /** @class */ (function (_super) {
__extends(JwtError, _super);
function JwtError(code, message) {
var _this = _super.call(this, message) || this;
_this.code = code;
_this.message = message;
_this.__proto__ = JwtError.prototype;
return _this;
}
return JwtError;
}(Error));
exports.JwtError = JwtError;
/**
* JWT error codes.
*/
var JwtErrorCode;
(function (JwtErrorCode) {
JwtErrorCode["INVALID_ARGUMENT"] = "invalid-argument";
JwtErrorCode["INVALID_CREDENTIAL"] = "invalid-credential";
JwtErrorCode["TOKEN_EXPIRED"] = "token-expired";
JwtErrorCode["INVALID_SIGNATURE"] = "invalid-token";
JwtErrorCode["NO_MATCHING_KID"] = "no-matching-kid-error";
JwtErrorCode["NO_KID_IN_HEADER"] = "no-kid-error";
JwtErrorCode["KEY_FETCH_ERROR"] = "key-fetch-error";
})(JwtErrorCode = exports.JwtErrorCode || (exports.JwtErrorCode = {}));