/*! firebase-admin v6.0.0 */ "use strict"; /*! * Copyright 2018 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"); // Audience to use for Firebase Auth Custom tokens var FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; exports.ALGORITHM_RS256 = 'RS256'; // 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'; // URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. var SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; /** User facing token information related to the Firebase ID token. */ exports.ID_TOKEN_INFO = { url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', verifyApiName: 'verifyIdToken()', jwtName: 'Firebase ID token', shortName: 'ID token', expiredErrorCode: 'auth/id-token-expired', }; /** User facing token information related to the Firebase session cookie. */ exports.SESSION_COOKIE_INFO = { url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', verifyApiName: 'verifySessionCookie()', jwtName: 'Firebase session cookie', shortName: 'session cookie', expiredErrorCode: 'auth/session-cookie-expired', }; /** * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies. */ var FirebaseTokenVerifier = /** @class */ (function () { function FirebaseTokenVerifier(clientCertUrl, algorithm, issuer, projectId, tokenInfo) { this.clientCertUrl = clientCertUrl; this.algorithm = algorithm; this.issuer = issuer; this.projectId = projectId; this.tokenInfo = tokenInfo; if (!validator.isURL(clientCertUrl)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The provided public client certificate URL is an invalid URL."); } else if (!validator.isNonEmptyString(algorithm)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The provided JWT algorithm is an empty string."); } else if (!validator.isURL(issuer)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The provided JWT issuer is an invalid URL."); } else if (!validator.isNonNullObject(tokenInfo)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The provided JWT information is not an object or null."); } else if (!validator.isURL(tokenInfo.url)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The provided JWT verification documentation URL is invalid."); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The JWT verify API name must be a non-empty string."); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The JWT public full name must be a non-empty string."); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The JWT public short name must be a non-empty string."); } else if (!validator.isNonEmptyString(tokenInfo.expiredErrorCode)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "The JWT expiration error code must be a non-empty string."); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; // For backward compatibility, the project ID is validated in the verification call. } /** * Verifies the format and signature of a Firebase Auth JWT token. * * @param {string} jwtToken The Firebase Auth JWT token to verify. * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID * token. */ FirebaseTokenVerifier.prototype.verifyJWT = function (jwtToken) { var _this = this; if (!validator.isString(jwtToken)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, "First argument to " + this.tokenInfo.verifyApiName + " must be a " + this.tokenInfo.jwtName + " string."); } if (!validator.isNonEmptyString(this.projectId)) { throw new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_CREDENTIAL, "Must initialize app with a cert credential or set your Firebase project ID as the " + ("GOOGLE_CLOUD_PROJECT environment variable to call " + this.tokenInfo.verifyApiName + ".")); } var fullDecodedToken = jwt.decode(jwtToken, { complete: true, }); var header = fullDecodedToken && fullDecodedToken.header; var payload = fullDecodedToken && fullDecodedToken.payload; var projectIdMatchMessage = " Make sure the " + this.tokenInfo.shortName + " comes from the same " + "Firebase project as the service account used to authenticate this SDK."; var verifyJwtTokenDocsMessage = " See " + this.tokenInfo.url + " " + ("for details on how to retrieve " + this.shortNameArticle + " " + this.tokenInfo.shortName + "."); var errorMessage; if (!fullDecodedToken) { errorMessage = "Decoding " + this.tokenInfo.jwtName + " failed. Make sure you passed the entire string JWT " + ("which represents " + this.shortNameArticle + " " + this.tokenInfo.shortName + ".") + verifyJwtTokenDocsMessage; } 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 = this.tokenInfo.verifyApiName + " expects " + this.shortNameArticle + " " + (this.tokenInfo.shortName + ", but was given a custom token."); } else if (isLegacyCustomToken) { errorMessage = this.tokenInfo.verifyApiName + " expects " + this.shortNameArticle + " " + (this.tokenInfo.shortName + ", but was given a legacy custom token."); } else { errorMessage = 'Firebase ID token has no "kid" claim.'; } errorMessage += verifyJwtTokenDocsMessage; } else if (header.alg !== this.algorithm) { errorMessage = this.tokenInfo.jwtName + " has incorrect algorithm. Expected \"" + this.algorithm + "\" but got " + "\"" + header.alg + "\"." + verifyJwtTokenDocsMessage; } else if (payload.aud !== this.projectId) { errorMessage = this.tokenInfo.jwtName + " has incorrect \"aud\" (audience) claim. Expected \"" + this.projectId + "\" but got \"" + payload.aud + "\"." + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + this.projectId) { errorMessage = this.tokenInfo.jwtName + " has incorrect \"iss\" (issuer) claim. Expected " + ("\"" + this.issuer + "\"") + this.projectId + "\" but got \"" + payload.iss + "\"." + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (typeof payload.sub !== 'string') { errorMessage = this.tokenInfo.jwtName + " has no \"sub\" (subject) claim." + verifyJwtTokenDocsMessage; } else if (payload.sub === '') { errorMessage = this.tokenInfo.jwtName + " has an empty string \"sub\" (subject) claim." + verifyJwtTokenDocsMessage; } else if (payload.sub.length > 128) { errorMessage = this.tokenInfo.jwtName + " has \"sub\" (subject) claim longer than 128 characters." + verifyJwtTokenDocsMessage; } 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, _this.tokenInfo.jwtName + " has \"kid\" claim which does not correspond to a known public key. " + ("Most likely the " + _this.tokenInfo.shortName + " is expired, so get a fresh token from your ") + "client app and try again.")); } else { return _this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); } }); }; /** * Verifies the JWT signature using the provided public key. * @param {string} jwtToken The JWT token to verify. * @param {string} publicKey The public key certificate. * @return {Promise} A promise that resolves with the decoded JWT claims on successful * verification. */ FirebaseTokenVerifier.prototype.verifyJwtSignatureWithKey = function (jwtToken, publicKey) { var _this = this; var errorMessage; var verifyJwtTokenDocsMessage = " See " + this.tokenInfo.url + " " + ("for details on how to retrieve " + this.shortNameArticle + " " + this.tokenInfo.shortName + "."); return new Promise(function (resolve, reject) { jwt.verify(jwtToken, publicKey, { algorithms: [_this.algorithm], }, function (error, decodedToken) { if (error) { if (error.name === 'TokenExpiredError') { errorMessage = _this.tokenInfo.jwtName + " has expired. Get a fresh token from your client " + ("app and try again (" + _this.tokenInfo.expiredErrorCode + ").") + verifyJwtTokenDocsMessage; } else if (error.name === 'JsonWebTokenError') { errorMessage = _this.tokenInfo.jwtName + " has invalid signature." + verifyJwtTokenDocsMessage; } return reject(new error_1.FirebaseAuthError(error_1.AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); } else { decodedToken.uid = decodedToken.sub; resolve(decodedToken); } }); }); }; /** * Fetches the public keys for the Google certs. * * @return {Promise} A promise fulfilled with public keys for the Google certs. */ FirebaseTokenVerifier.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) { var https = require('https'); https.get(_this.clientCertUrl, 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 FirebaseTokenVerifier; }()); exports.FirebaseTokenVerifier = FirebaseTokenVerifier; /** * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. * * @param {string} projectId Project ID string. * @return {FirebaseTokenVerifier} */ function createIdTokenVerifier(projectId) { return new FirebaseTokenVerifier(CLIENT_CERT_URL, exports.ALGORITHM_RS256, 'https://securetoken.google.com/', projectId, exports.ID_TOKEN_INFO); } exports.createIdTokenVerifier = createIdTokenVerifier; /** * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. * * @param {string} projectId Project ID string. * @return {FirebaseTokenVerifier} */ function createSessionCookieVerifier(projectId) { return new FirebaseTokenVerifier(SESSION_COOKIE_CERT_URL, exports.ALGORITHM_RS256, 'https://session.firebase.google.com/', projectId, exports.SESSION_COOKIE_INFO); } exports.createSessionCookieVerifier = createSessionCookieVerifier;