npmtest-firebase (v0.0.1)

Code coverage report for node-npmtest-firebase/firebase/server-auth-node/token-generator.js

Statements: 12.82% (15 / 117)      Branches: 3.3% (3 / 91)      Functions: 0% (0 / 12)      Lines: 12.82% (15 / 117)      Ignored: 1 statement, 3 branches     

All files » node-npmtest-firebase/firebase/server-auth-node/ » token-generator.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259          1 1 1 1     1 1     1             1     1                 1                                                             1                                                                                       1                                                                                                                                                                                         1                                                                                         1               1      
/*! @license Firebase v3.7.5
    Build: 3.7.5-rc.1
    Terms: https://developers.google.com/terms */
'use strict';
 
var fs = require('fs');
var https = require('https');
var jwt = require('jsonwebtoken');
var firebase = require('../app-node');
 
 
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).
 *
 * @constructor
 * @param {string|Object} serviceAccount Either the path to a service account key or the service account key itself.
 */
var FirebaseTokenGenerator = function(serviceAccount) {
  serviceAccount = serviceAccount || process.env.GOOGLE_APPLICATION_CREDENTIALS;
  if (typeof serviceAccount === 'string') {
    try {
      this.serviceAccount = JSON.parse(fs.readFileSync(serviceAccount, 'utf8'));
    } catch (error) {
      throw new Error('Failed to parse service account key file: ' + error);
    }
  } else if (typeof serviceAccount === 'object' && serviceAccount !== null) {
    this.serviceAccount = serviceAccount;
  } else {
    throw new Error('Must provide a service account to use FirebaseTokenGenerator');
  }
 
  if (typeof this.serviceAccount.private_key !== 'string' || this.serviceAccount.private_key === '') {
    throw new Error('Service account key must contain a string "private_key" field');
  } else if (typeof this.serviceAccount.client_email !== 'string' || this.serviceAccount.client_email === '') {
    throw new Error('Service account key must contain a string "client_email" field');
  }
};
 
 
/**
 * 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 {string} A Firebase Auth Custom token signed with a service account key and containing
 *                  the provided payload.
 */
FirebaseTokenGenerator.prototype.createCustomToken = function(uid, developerClaims) {
  if (typeof uid !== 'string' || uid === '') {
    throw new Error('First argument to createCustomToken() must be a non-empty string uid');
  } else if (uid.length > 128) {
    throw new Error('First argument to createCustomToken() must a uid with less than or equal to 128 characters');
  } else if (typeof developerClaims !== 'undefined' && (typeof developerClaims !== 'object' || developerClaims === null || developerClaims instanceof Array)) {
    throw new Error('Optional second argument to createCustomToken() must be an object containing the developer claims');
  }
 
  var jwtPayload = {};
 
  if (typeof developerClaims !== 'undefined') {
    jwtPayload.claims = {};
 
    for (var key in developerClaims) {
      /* istanbul ignore else */
      if (developerClaims.hasOwnProperty(key)) {
        if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) {
          throw new Error('Developer claim "' + key + '" is reserved and cannot be specified');
        }
 
        jwtPayload.claims[key] = developerClaims[key];
      }
    }
  }
  jwtPayload.uid = uid;
 
  return jwt.sign(jwtPayload, this.serviceAccount.private_key, {
    audience: FIREBASE_AUDIENCE,
    expiresIn: ONE_HOUR_IN_SECONDS,
    issuer: this.serviceAccount.client_email,
    subject: this.serviceAccount.client_email,
    algorithm: ALGORITHM
  });
};
 
 
/**
 * 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('First argument to verifyIdToken() must be a Firebase ID token');
  }
 
  if (typeof this.serviceAccount.project_id !== 'string' || this.serviceAccount.project_id === '') {
    throw new Error('verifyIdToken() requires a service account 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/server/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.serviceAccount.project_id) {
    errorMessage = 'Firebase ID token has incorrect "aud" (audience) claim. Expected "' + this.serviceAccount.project_id +
      '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyIdTokenDocsMessage;
  } else if (payload.iss !== 'https://securetoken.google.com/' + this.serviceAccount.project_id) {
    errorMessage = 'Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/' +
      this.serviceAccount.project_id + '" 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 firebase.Promise.reject(new Error(errorMessage));
  }
 
  return this._fetchPublicKeys().then(function(publicKeys) {
    if (!publicKeys.hasOwnProperty(header.kid)) {
      return firebase.Promise.reject('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 firebase.Promise(function(resolve, reject) {
      jwt.verify(idToken, publicKeys[header.kid], {
        algorithms: [ALGORITHM]
      }, function(error, decodedToken) {
        if (error) {
          if (error.name === 'TokenExpiredError') {
            error = 'Firebase ID token has expired. Get a fresh token from your client app and try ' +
              'again.' + verifyIdTokenDocsMessage;
          } else if (error.name === 'JsonWebTokenError') {
            error = 'Firebase ID token has invalid signature.' + verifyIdTokenDocsMessage;
          }
          reject(error);
        } else {
          decodedToken.uid = decodedToken.sub;
          resolve(decodedToken);
        }
      });
    });
  });
};
 
 
/**
 * 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() {
  if (typeof this._publicKeys !== 'undefined' && typeof this._publicKeysExpireAt !== 'undefined' && Date.now() < this._publicKeysExpireAt) {
    return firebase.Promise.resolve(this._publicKeys);
  }
 
  var self = this;
 
  return new firebase.Promise(function(resolve, reject) {
    https.get(CLIENT_CERT_URL, function(res) {
      var buffers = [];
 
      res.on('data', function(buffer) {
        buffers.push(buffer);
      });
 
      res.on('end', function() {
        try {
          var response = JSON.parse(Buffer.concat(buffers));
 
          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(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];
                  self._publicKeysExpireAt = Date.now() + (maxAge * 1000);
                }
              });
            }
 
            self._publicKeys = response;
            resolve(response);
          }
        } catch (e) {
          /* istanbul ignore next */
          reject(e);
        }
      });
    }).on('error', reject);
  });
};
 
 
module.exports = FirebaseTokenGenerator;