/*! firebase-admin v10.3.0 */
"use strict";
/*!
 * 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 = {}));
