All files / src/token PrincipalToken.js

98.82% Statements 167/169
96.3% Branches 78/81
92.86% Functions 13/14
98.82% Lines 167/169
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 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382                              3x 3x 3x 3x       153x   153x   153x 153x 153x 153x 153x 153x 153x   153x 69x 69x   6x     84x 84x   15x                       72x   72x 3x                                                     69x 69x 69x     69x 69x 930x 930x 921x   69x 69x   57x 57x   45x 45x   33x 33x   39x 39x   66x 66x   69x 69x   54x 54x   54x 54x   69x 69x   66x 66x   54x 54x   69x 69x   69x 69x   69x 69x   39x 39x                     69x 3x     66x 3x     63x   63x                           63x 57x                     84x 9x     69x 69x 69x 69x 69x 69x 69x 69x 69x 69x   69x   69x 69x 69x 69x 69x 69x   69x 69x 69x 69x 69x 21x   69x 66x   69x 66x   69x 63x   69x 69x             72x 6x     66x 66x   66x       63x 63x     66x     66x 66x       36x 36x 3x   3x 3x     33x 33x 3x   3x 3x     30x 30x 3x   3x 3x     27x 27x 27x 24x 9x   9x   24x     3x       27x       36x     36x     6x 3x       3x   3x 3x         30x 6x   6x 6x           24x 18x 3x     3x 3x   6x 3x   3x 3x     18x       18x       18x       9x       18x       45x       24x       15x       3x  
/**
 * 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.
 */
'use strict';
 
var winston = require('winston');
var Token = require('./Token');
var Crypto = require('../util/Crypto');
var config = require('../../config/config')();
 
class PrincipalToken extends Token {
  constructor(token) {
    super();
 
    winston.level = config.logLevel;
 
    this._name = null;
    this._originalRequestor = null;
    this._keyService = null;
    this._authorizedServices = null;
    this._authorizedServiceName = null;
    this._authorizedServiceKeyId = '0';
    this._authorizedServiceSignature = null;
 
    if (typeof token === 'string') {
      try {
        this.parseSignedToken(token);
      } catch (e) {
        throw e;
      }
    } else {
      try {
        this.builder(token);
      } catch (e) {
        throw e;
      }
    }
  }
 
  static setConfig(c) {
    super.setConfig(c);
    config = Object.assign({}, config, c.auth_core);
  }
 
  /*eslint complexity: ["error", 24]*/
  parseSignedToken(signedToken) {
    winston.debug('Constructing PrincipalToken with input string: ' + signedToken);
 
    if (!signedToken) {
      throw new Error('Input String signedToken must not be empty');
    }
 
    /**
     * first we need to extract data and signature parts
     * the signature is always at the end of the token. The principal
     * token can represent 2 types - service or user. The version
     * string identifies the type by using S or U. Here are two sample
     * tokens:
     *
     * User:
     * v=U1;d=user;n=john;a=salt;t=tstamp;e=expiry;s=sig
     *
     * Service:
     * v=S1;d=sports;n=storage;h=host.somecompany.com;a=salt;t=tstamp;e=expiry;s=sig
     *
     * v: version number U1 or S1 (string)
     * d: domain name (as passed by the client) or user for users
     * n: service name (as passed by the client) or username
     * h: hostname or IP address (string)
     * a: random 8 byte salt value hex encoded
     * t: timestamp when the token was generated
     * e: expiry timestamp based on SIA configuration
     * s: signature generated over the "v=U1;a=salt;...;e=expiry" string
     *    using Service's private Key for service tokens and ZMS service's
     *    private key for user tokens and y64 encoded
     */
    var idx = signedToken.indexOf(';s=');
    Eif (idx !== -1) {
      this._unsignedToken = signedToken.substring(0, idx);
    }
 
    var item = signedToken.split(';');
    for (var i = 0; i < item.length; i++) {
      var kv = item[i].split('=');
      if (kv.length === 2) {
        switch (kv[0]) {
          case 'a':
            this._salt = kv[1];
            break;
          case 'b':
            this._authorizedServices = kv[1].split(',');
            break;
          case 'bk':
            this._authorizedServiceKeyId = kv[1];
            break;
          case 'bn':
            this._authorizedServiceName = kv[1];
            break;
          case 'bs':
            this._authorizedServiceSignature = kv[1];
            break;
          case 'd':
            this._domain = kv[1];
            break;
          case 'e':
            this._expiryTime = Number(kv[1]);
            break;
          case 'h':
            this._host = kv[1];
            break;
          case 'i':
            this._ip = kv[1];
            break;
          case 'k':
            this._keyId = kv[1];
            break;
          case 'n':
            this._name = kv[1];
            break;
          case 'o':
            this._originalRequestor = kv[1];
            break;
          case 's':
            this._signature = kv[1];
            break;
          case 't':
            this._timestamp = Number(kv[1]);
            break;
          case 'v':
            this._version = kv[1];
            break;
          case 'z':
            this._keyService = kv[1];
            break;
        }
      }
    }
 
    /* the required attributes for the token are
     * domain and roles. The signature will be verified
     * during the authenticate phase but now we'll make
     * sure that domain and roles are present
     */
 
    if (!this._domain) {
      throw new Error('SignedToken does not contain required domain component');
    }
 
    if (!this._name) {
      throw new Error('SignedToken does not contain required name component');
    }
 
    this._signedToken = signedToken;
 
    winston.debug(
      'Values extracted from token ' +
      ' version:' + this._version +
      ' domain:' + this._domain +
      ' service:' + this._name +
      ' host:' + this._host +
      ' ip: ' + this._ip +
      ' id: ' + this._keyId +
      ' keyService: ' + this._keyService +
      ' originalRequestor: ' + this._originalRequestor +
      ' salt:' + this._salt +
      ' timestamp:' + this._timestamp +
      ' expiryTime:' + this._expiryTime +
      ' signature:' + this._signature);
    if (this._authorizedServices) {
      winston.debug(
        'Authorized service details from token ' +
        ' authorizedServices:' + this._authorizedServices.join(',') +
        ' authorizedServiceName:' + this._authorizedServiceName +
        ' authorizedServiceKeyId:' + this._authorizedServiceKeyId +
        ' authorizedServiceSignature:' + this._authorizedServiceSignature
      );
    }
  }
 
  builder(options) {
    if (!options.version || !options.domain || !options.name) {
      throw new Error('version, domain and name parameters must not be null.');
    }
 
    this._version = options.version;
    this._domain = options.domain;
    this._name = options.name;
    this._host = options.host;
    this._salt = (options.salt || Crypto.randomSalt());
    this._keyId = (options.keyId || '0');
    this._ip = options.ip;
    this._authorizedServices = (options.authorizedServices) ? options.authorizedServices.split(',') : null;
    this._keyService = options.keyService;
    this._originalRequestor = options.originalRequestor;
 
    this.setTimeStamp((options.issueTime || 0), (options.expirationWindow || 3600));
 
    var parts = [];
    parts.push('v=' + this._version);
    parts.push('d=' + this._domain);
    parts.push('n=' + this._name);
    Eif (this._host) {
      parts.push('h=' + this._host);
    }
    parts.push('a=' + this._salt);
    parts.push('t=' + this._timestamp);
    parts.push('e=' + this._expiryTime);
    parts.push('k=' + this._keyId);
    if (this._keyService) {
      parts.push('z=' + this._keyService);
    }
    if (this._originalRequestor) {
      parts.push('o=' + this._originalRequestor);
    }
    if (this._ip) {
      parts.push('i=' + this._ip);
    }
    if (this._authorizedServices) {
      parts.push('b=' + this._authorizedServices.join(','));
    }
    this._unsignedToken = parts.join(';');
    winston.debug('PrincipalToken created: ' + this._unsignedToken);
  }
 
  signForAuthorizedService(authorizedServiceName, authorizedServiceKeyId, privateKey) {
 
    /* first let's make sure the authorized service is one of the
     * listed service names in the PrincipalToken */
    if (!this._authorizedServices || this._authorizedServices.indexOf(authorizedServiceName) === -1) {
      throw new Error('Authorized Service is not valid for this token');
    }
 
    this._authorizedServiceKeyId = authorizedServiceKeyId;
    var tokenToSign = this._signedToken + ';bk=' + authorizedServiceKeyId;
 
    if (this._authorizedServices.length > 1) {
      /* if the user has allowed multiple authorized services then we need
       * to keep track of which one is re-signing this._token and as such
       * we'll store the service name as the value for the bn field */
      this._authorizedServiceName = authorizedServiceName;
      tokenToSign += ';bn=' + authorizedServiceName;
    }
 
    this._authorizedServiceSignature = Crypto.sign(tokenToSign, privateKey, this._digestAlgorithm);
 
    /* now append our new signature to the token we just signed */
    tokenToSign += ';bs=' + this._authorizedServiceSignature;
    this._signedToken = tokenToSign;
  }
 
  validateForAuthorizedService(publicKey) {
    var err = null;
    if (!this._authorizedServiceSignature) {
      err = new Error('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
        ' : missing data/signature component: public key=' + publicKey);
      winston.error(err);
      return false;
    }
 
    var idx = this._signedToken.indexOf(';bs=');
    if (idx === -1) {
      err = new Error('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
        ' : not signed by any authorized service');
      winston.error(err);
      return false;
    }
 
    var unsignedAuthorizedServiceToken = this._signedToken.substring(0, idx);
    if (!publicKey) {
      err = new Error('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
        ' : No public key provided');
      winston.error(err);
      return false;
    }
 
    var verified = false; // fail safe
    try {
      verified = Crypto.verify(unsignedAuthorizedServiceToken, publicKey, this._authorizedServiceSignature, this._digestAlgorithm);
      if (verified === false) {
        err = new Error('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
          ' : authentication failed: public key=' + publicKey);
        winston.error(err);
      }
      winston.debug('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
        ' -  successfully authenticated');
    } catch (e) {
      winston.error('PrincipalToken:validateForAuthorizedService: token=' + this._unsignedToken +
        ' : authentication failed verifying signature: exc=' + e.message +
        ' : public key=' + publicKey);
    }
    return verified;
  }
 
  isValidAuthorizedServiceToken() {
    var err = null;
 
    /* we start our by checking if this is an authorized service token */
    if (!this._authorizedServices) {
      /* if both the service name list and signature are not present
       * then we have a standard principal token */
      if (!this._authorizedServiceSignature) {
        return true;
      }
 
      /* otherwise we have an invalid token without the signature */
      err = new Error('PrincipalToken:isValidAuthorizedServiceToken: Invalid Token=' + this._unsignedToken +
        ' : Authorized Service Signature available without service name');
      winston.error(err);
      return false;
    }
 
    /* if we have an authorized service name then we must have a corresponding
     * signature available in the token */
    if (!this._authorizedServiceSignature) {
      err = new Error('PrincipalToken:isValidAuthorizedServiceToken: Invalid Token=' + this._unsignedToken +
        ' : Missing signature for specified authorized service');
      winston.error(err);
      return false;
    }
 
    /* if we have a specific authorized service name specified then
     * it must be present in our service list otherwise we must
     * have a single entry in our list */
    if (this._authorizedServiceName) {
      if (this._authorizedServices.indexOf(this._authorizedServiceName) === -1) {
        err = new Error('PrincipalToken:isValidAuthorizedServiceToken: Invalid Token=' + this._unsignedToken +
          ' : Authorized service name=' + this._authorizedServiceName +
          ' is not listed in the service list');
        winston.error(err);
        return false;
      }
    } else if (this._authorizedServices.length !== 1) {
      err = new Error('PrincipalToken:isValidAuthorizedServiceToken: Invalid Token=' + this._unsignedToken +
        ' : No service name and Authorized service list contains multiple entries');
      winston.error(err);
      return false;
    }
 
    return true;
  }
 
  getName() {
    return this._name;
  }
 
  getKeyService() {
    return this._keyService;
  }
 
  getOriginalRequestor() {
    return this._originalRequestor;
  }
 
  getAuthorizedServices() {
    return this._authorizedServices;
  }
 
  getAuthorizedServiceName() {
    return this._authorizedServiceName;
  }
 
  getAuthorizedServiceKeyId() {
    return this._authorizedServiceKeyId;
  }
 
  getAuthorizedServiceSignature() {
    return this._authorizedServiceSignature;
  }
}
 
module.exports = PrincipalToken;