/*! firebase-admin v5.2.1 */
"use strict";
/*!
 * Copyright 2017 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
var error_1 = require("../utils/error");
var validator = require("../utils/validator");
var jwt = require("jsonwebtoken");
// Use untyped import syntax for Node built-ins
var https = require("https");
var ALGORITHM = 'RS256';
var ONE_HOUR_IN_SECONDS = 60 * 60;
// List of blacklisted claims which cannot be provided when creating a custom token
var BLACKLISTED_CLAIMS = [
    'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti',
    'nbf', 'nonce',
];
// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
// Auth ID tokens)
var CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
// Audience to use for Firebase Auth Custom tokens
var FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
/**
 * Class for generating and verifying different types of Firebase Auth tokens (JWTs).
 */
var FirebaseTokenGenerator = (function () {
    function FirebaseTokenGenerator(certificate) {
        if (!certificate) {
            throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, 'INTERNAL ASSERT: Must provide a certificate to use FirebaseTokenGenerator.');
        }
        this.certificate_ = certificate;
    }
    /**
     * Creates a new Firebase Auth Custom token.
     *
     * @param {string} uid The user ID to use for the generated Firebase Auth Custom token.
     * @param {Object} [developerClaims] Optional developer claims to include in the generated Firebase
     *                 Auth Custom token.
     * @return {Promise<string>} A Promise fulfilled with a Firebase Auth Custom token signed with a
     *                           service account key and containing the provided payload.
     */
    FirebaseTokenGenerator.prototype.createCustomToken = function (uid, developerClaims) {
        var errorMessage;
        if (typeof uid !== 'string' || uid === '') {
            errorMessage = 'First argument to createCustomToken() must be a non-empty string uid.';
        }
        else if (uid.length > 128) {
            errorMessage = 'First argument to createCustomToken() must a uid with less than or equal to 128 characters.';
        }
        else if (!this.isDeveloperClaimsValid_(developerClaims)) {
            errorMessage = 'Second argument to createCustomToken() must be an object containing the developer claims.';
        }
        if (typeof errorMessage !== 'undefined') {
            throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage);
        }
        if (!validator.isNonEmptyString(this.certificate_.privateKey)) {
            errorMessage = 'createCustomToken() requires a certificate with "private_key" set.';
        }
        else if (!validator.isNonEmptyString(this.certificate_.clientEmail)) {
            errorMessage = 'createCustomToken() requires a certificate with "client_email" set.';
        }
        if (typeof errorMessage !== 'undefined') {
            throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, errorMessage);
        }
        var jwtPayload = {};
        if (typeof developerClaims !== 'undefined') {
            var claims = {};
            for (var key in developerClaims) {
                /* istanbul ignore else */
                if (developerClaims.hasOwnProperty(key)) {
                    if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) {
                        throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "Developer claim \"" + key + "\" is reserved and cannot be specified.");
                    }
                    claims[key] = developerClaims[key];
                }
            }
            jwtPayload.claims = claims;
        }
        jwtPayload.uid = uid;
        var customToken = jwt.sign(jwtPayload, this.certificate_.privateKey, {
            audience: FIREBASE_AUDIENCE,
            expiresIn: ONE_HOUR_IN_SECONDS,
            issuer: this.certificate_.clientEmail,
            subject: this.certificate_.clientEmail,
            algorithm: ALGORITHM,
        });
        return Promise.resolve(customToken);
    };
    /**
     * Verifies the format and signature of a Firebase Auth ID token.
     *
     * @param {string} idToken The Firebase Auth ID token to verify.
     * @return {Promise<Object>} A promise fulfilled with the decoded claims of the Firebase Auth ID
     *                           token.
     */
    FirebaseTokenGenerator.prototype.verifyIdToken = function (idToken) {
        if (typeof idToken !== 'string') {
            throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'First argument to verifyIdToken() must be a Firebase ID token string.');
        }
        if (!validator.isNonEmptyString(this.certificate_.projectId)) {
            throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, 'verifyIdToken() requires a certificate with "project_id" set.');
        }
        var fullDecodedToken = jwt.decode(idToken, {
            complete: true,
        });
        var header = fullDecodedToken && fullDecodedToken.header;
        var payload = fullDecodedToken && fullDecodedToken.payload;
        var projectIdMatchMessage = ' Make sure the ID token comes from the same Firebase project as the ' +
            'service account used to authenticate this SDK.';
        var verifyIdTokenDocsMessage = ' See https://firebase.google.com/docs/auth/admin/verify-id-tokens ' +
            'for details on how to retrieve an ID token.';
        var errorMessage;
        if (!fullDecodedToken) {
            errorMessage = 'Decoding Firebase ID token failed. Make sure you passed the entire string JWT ' +
                'which represents an ID token.' + verifyIdTokenDocsMessage;
        }
        else if (typeof header.kid === 'undefined') {
            var isCustomToken = (payload.aud === FIREBASE_AUDIENCE);
            var isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d);
            if (isCustomToken) {
                errorMessage = 'verifyIdToken() expects an ID token, but was given a custom token.';
            }
            else if (isLegacyCustomToken) {
                errorMessage = 'verifyIdToken() expects an ID token, but was given a legacy custom token.';
            }
            else {
                errorMessage = 'Firebase ID token has no "kid" claim.';
            }
            errorMessage += verifyIdTokenDocsMessage;
        }
        else if (header.alg !== ALGORITHM) {
            errorMessage = 'Firebase ID token has incorrect algorithm. Expected "' + ALGORITHM + '" but got ' +
                '"' + header.alg + '".' + verifyIdTokenDocsMessage;
        }
        else if (payload.aud !== this.certificate_.projectId) {
            errorMessage = 'Firebase ID token has incorrect "aud" (audience) claim. Expected "' +
                this.certificate_.projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage +
                verifyIdTokenDocsMessage;
        }
        else if (payload.iss !== 'https://securetoken.google.com/' + this.certificate_.projectId) {
            errorMessage = 'Firebase ID token has incorrect "iss" (issuer) claim. Expected ' +
                '"https://securetoken.google.com/' + this.certificate_.projectId + '" but got "' +
                payload.iss + '".' + projectIdMatchMessage + verifyIdTokenDocsMessage;
        }
        else if (typeof payload.sub !== 'string') {
            errorMessage = 'Firebase ID token has no "sub" (subject) claim.' + verifyIdTokenDocsMessage;
        }
        else if (payload.sub === '') {
            errorMessage = 'Firebase ID token has an empty string "sub" (subject) claim.' + verifyIdTokenDocsMessage;
        }
        else if (payload.sub.length > 128) {
            errorMessage = 'Firebase ID token has "sub" (subject) claim longer than 128 characters.' +
                verifyIdTokenDocsMessage;
        }
        if (typeof errorMessage !== 'undefined') {
            return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
        }
        return this.fetchPublicKeys_().then(function (publicKeys) {
            if (!publicKeys.hasOwnProperty(header.kid)) {
                return Promise.reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, 'Firebase ID token has "kid" claim which does not correspond to a known public key. ' +
                    'Most likely the ID token is expired, so get a fresh token from your client app and ' +
                    'try again.' + verifyIdTokenDocsMessage));
            }
            return new Promise(function (resolve, reject) {
                jwt.verify(idToken, publicKeys[header.kid], {
                    algorithms: [ALGORITHM],
                }, function (error, decodedToken) {
                    if (error) {
                        if (error.name === 'TokenExpiredError') {
                            errorMessage = 'Firebase ID token has expired. Get a fresh token from your client app and try ' +
                                'again.' + verifyIdTokenDocsMessage;
                        }
                        else if (error.name === 'JsonWebTokenError') {
                            errorMessage = 'Firebase ID token has invalid signature.' + verifyIdTokenDocsMessage;
                        }
                        return reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
                    }
                    else {
                        decodedToken.uid = decodedToken.sub;
                        resolve(decodedToken);
                    }
                });
            });
        });
    };
    /**
     * Returns whether or not the provided developer claims are valid.
     *
     * @param {Object} [developerClaims] Optional developer claims to validate.
     * @return {boolean} True if the provided claims are valid; otherwise, false.
     */
    FirebaseTokenGenerator.prototype.isDeveloperClaimsValid_ = function (developerClaims) {
        if (typeof developerClaims === 'undefined') {
            return true;
        }
        if (typeof developerClaims === 'object' && developerClaims !== null && !(developerClaims instanceof Array)) {
            return true;
        }
        return false;
    };
    /**
     * Fetches the public keys for the Google certs.
     *
     * @return {Promise<Object>} A promise fulfilled with public keys for the Google certs.
     */
    FirebaseTokenGenerator.prototype.fetchPublicKeys_ = function () {
        var _this = this;
        var publicKeysExist = (typeof this.publicKeys_ !== 'undefined');
        var publicKeysExpiredExists = (typeof this.publicKeysExpireAt_ !== 'undefined');
        var publicKeysStillValid = (publicKeysExpiredExists && Date.now() < this.publicKeysExpireAt_);
        if (publicKeysExist && publicKeysStillValid) {
            return Promise.resolve(this.publicKeys_);
        }
        return new Promise(function (resolve, reject) {
            https.get(CLIENT_CERT_URL, function (res) {
                var buffers = [];
                res.on('data', function (buffer) { return buffers.push(buffer); });
                res.on('end', function () {
                    try {
                        var response = JSON.parse(Buffer.concat(buffers).toString());
                        if (response.error) {
                            var errorMessage = 'Error fetching public keys for Google certs: ' + response.error;
                            /* istanbul ignore else */
                            if (response.error_description) {
                                errorMessage += ' (' + response.error_description + ')';
                            }
                            reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INTERNAL_ERROR, errorMessage));
                        }
                        else {
                            /* istanbul ignore else */
                            if (res.headers.hasOwnProperty('cache-control')) {
                                var cacheControlHeader = res.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_ = response;
                            resolve(response);
                        }
                    }
                    catch (e) {
                        /* istanbul ignore next */
                        reject(e);
                    }
                });
            }).on('error', reject);
        });
    };
    return FirebaseTokenGenerator;
}());
exports.FirebaseTokenGenerator = FirebaseTokenGenerator;
