UNPKG

firebase-admin

Version:
270 lines (269 loc) 14.2 kB
/*! firebase-admin v5.11.0 */ "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 = /** @class */ (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 (auth/id-token-expired).' + 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;