mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-30 18:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			215 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			215 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var timespan = require('./lib/timespan');
 | |
| var PS_SUPPORTED = true
 | |
| var jws = require('../jws');
 | |
| var once = require('../lodash.once');
 | |
| 
 | |
| var SUPPORTED_ALGS = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none'];
 | |
| if (PS_SUPPORTED) {
 | |
|   SUPPORTED_ALGS.splice(3, 0, 'PS256', 'PS384', 'PS512');
 | |
| }
 | |
| 
 | |
| function isPlainObject(value) {
 | |
|   var type = typeof value;
 | |
|   return !!value && type == 'object' && !Array.isArray(value);
 | |
| }
 | |
| function isInteger(val) {
 | |
|   return !isNaN(val) && val !== null && !String(val).includes('.')
 | |
| }
 | |
| function isNumber(val) {
 | |
|   return !isNaN(val) && val !== null
 | |
| }
 | |
| function isString(val) {
 | |
|   return typeof val == 'string'
 | |
| }
 | |
| 
 | |
| var sign_options_schema = {
 | |
|   expiresIn: { isValid: function (value) { return isInteger(value) || (isString(value) && value); }, message: '"expiresIn" should be a number of seconds or string representing a timespan' },
 | |
|   notBefore: { isValid: function (value) { return isInteger(value) || (isString(value) && value); }, message: '"notBefore" should be a number of seconds or string representing a timespan' },
 | |
|   audience: { isValid: function (value) { return isString(value) || Array.isArray(value); }, message: '"audience" must be a string or array' },
 | |
|   algorithm: { isValid: function (value) { return SUPPORTED_ALGS.includes(value); }, message: '"algorithm" must be a valid string enum value' },
 | |
|   header: { isValid: isPlainObject, message: '"header" must be an object' },
 | |
|   encoding: { isValid: isString, message: '"encoding" must be a string' },
 | |
|   issuer: { isValid: isString, message: '"issuer" must be a string' },
 | |
|   subject: { isValid: isString, message: '"subject" must be a string' },
 | |
|   jwtid: { isValid: isString, message: '"jwtid" must be a string' },
 | |
|   noTimestamp: { isValid: function (value) { return value === true || value === false; }, message: '"noTimestamp" must be a boolean' },
 | |
|   keyid: { isValid: isString, message: '"keyid" must be a string' },
 | |
|   mutatePayload: { isValid: function (value) { return value === true || value === false; }, message: '"mutatePayload" must be a boolean' }
 | |
| };
 | |
| 
 | |
| var registered_claims_schema = {
 | |
|   iat: { isValid: isNumber, message: '"iat" should be a number of seconds' },
 | |
|   exp: { isValid: isNumber, message: '"exp" should be a number of seconds' },
 | |
|   nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
 | |
| };
 | |
| 
 | |
| function validate(schema, allowUnknown, object, parameterName) {
 | |
|   if (!isPlainObject(object)) {
 | |
|     throw new Error('Expected "' + parameterName + '" to be a plain object.');
 | |
|   }
 | |
|   Object.keys(object)
 | |
|     .forEach(function (key) {
 | |
|       var validator = schema[key];
 | |
|       if (!validator) {
 | |
|         if (!allowUnknown) {
 | |
|           throw new Error('"' + key + '" is not allowed in "' + parameterName + '"');
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|       if (!validator.isValid(object[key])) {
 | |
|         throw new Error(validator.message);
 | |
|       }
 | |
|     });
 | |
| }
 | |
| 
 | |
| function validateOptions(options) {
 | |
|   return validate(sign_options_schema, false, options, 'options');
 | |
| }
 | |
| 
 | |
| function validatePayload(payload) {
 | |
|   return validate(registered_claims_schema, true, payload, 'payload');
 | |
| }
 | |
| 
 | |
| var options_to_payload = {
 | |
|   'audience': 'aud',
 | |
|   'issuer': 'iss',
 | |
|   'subject': 'sub',
 | |
|   'jwtid': 'jti'
 | |
| };
 | |
| 
 | |
| var options_for_objects = [
 | |
|   'expiresIn',
 | |
|   'notBefore',
 | |
|   'noTimestamp',
 | |
|   'audience',
 | |
|   'issuer',
 | |
|   'subject',
 | |
|   'jwtid',
 | |
| ];
 | |
| 
 | |
| module.exports = function (payload, secretOrPrivateKey, options, callback) {
 | |
|   if (typeof options === 'function') {
 | |
|     callback = options;
 | |
|     options = {};
 | |
|   } else {
 | |
|     options = options || {};
 | |
|   }
 | |
| 
 | |
|   var isObjectPayload = typeof payload === 'object' &&
 | |
|     !Buffer.isBuffer(payload);
 | |
| 
 | |
|   var header = Object.assign({
 | |
|     alg: options.algorithm || 'HS256',
 | |
|     typ: isObjectPayload ? 'JWT' : undefined,
 | |
|     kid: options.keyid
 | |
|   }, options.header);
 | |
| 
 | |
|   function failure(err) {
 | |
|     if (callback) {
 | |
|       return callback(err);
 | |
|     }
 | |
|     throw err;
 | |
|   }
 | |
| 
 | |
|   if (!secretOrPrivateKey && options.algorithm !== 'none') {
 | |
|     return failure(new Error('secretOrPrivateKey must have a value'));
 | |
|   }
 | |
| 
 | |
|   if (typeof payload === 'undefined') {
 | |
|     return failure(new Error('payload is required'));
 | |
|   } else if (isObjectPayload) {
 | |
|     try {
 | |
|       validatePayload(payload);
 | |
|     }
 | |
|     catch (error) {
 | |
|       return failure(error);
 | |
|     }
 | |
|     if (!options.mutatePayload) {
 | |
|       payload = Object.assign({}, payload);
 | |
|     }
 | |
|   } else {
 | |
|     var invalid_options = options_for_objects.filter(function (opt) {
 | |
|       return typeof options[opt] !== 'undefined';
 | |
|     });
 | |
| 
 | |
|     if (invalid_options.length > 0) {
 | |
|       return failure(new Error('invalid ' + invalid_options.join(',') + ' option for ' + (typeof payload) + ' payload'));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (typeof payload.exp !== 'undefined' && typeof options.expiresIn !== 'undefined') {
 | |
|     return failure(new Error('Bad "options.expiresIn" option the payload already has an "exp" property.'));
 | |
|   }
 | |
| 
 | |
|   if (typeof payload.nbf !== 'undefined' && typeof options.notBefore !== 'undefined') {
 | |
|     return failure(new Error('Bad "options.notBefore" option the payload already has an "nbf" property.'));
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     validateOptions(options);
 | |
|   }
 | |
|   catch (error) {
 | |
|     return failure(error);
 | |
|   }
 | |
| 
 | |
|   var timestamp = payload.iat || Math.floor(Date.now() / 1000);
 | |
| 
 | |
|   if (options.noTimestamp) {
 | |
|     delete payload.iat;
 | |
|   } else if (isObjectPayload) {
 | |
|     payload.iat = timestamp;
 | |
|   }
 | |
| 
 | |
|   if (typeof options.notBefore !== 'undefined') {
 | |
|     try {
 | |
|       payload.nbf = timespan(options.notBefore, timestamp);
 | |
|     }
 | |
|     catch (err) {
 | |
|       return failure(err);
 | |
|     }
 | |
|     if (typeof payload.nbf === 'undefined') {
 | |
|       return failure(new Error('"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (typeof options.expiresIn !== 'undefined' && typeof payload === 'object') {
 | |
|     try {
 | |
|       payload.exp = timespan(options.expiresIn, timestamp);
 | |
|     }
 | |
|     catch (err) {
 | |
|       return failure(err);
 | |
|     }
 | |
|     if (typeof payload.exp === 'undefined') {
 | |
|       return failure(new Error('"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Object.keys(options_to_payload).forEach(function (key) {
 | |
|     var claim = options_to_payload[key];
 | |
|     if (typeof options[key] !== 'undefined') {
 | |
|       if (typeof payload[claim] !== 'undefined') {
 | |
|         return failure(new Error('Bad "options.' + key + '" option. The payload already has an "' + claim + '" property.'));
 | |
|       }
 | |
|       payload[claim] = options[key];
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var encoding = options.encoding || 'utf8';
 | |
| 
 | |
|   if (typeof callback === 'function') {
 | |
|     callback = callback && once(callback);
 | |
| 
 | |
|     jws.createSign({
 | |
|       header: header,
 | |
|       privateKey: secretOrPrivateKey,
 | |
|       payload: payload,
 | |
|       encoding: encoding
 | |
|     }).once('error', callback)
 | |
|       .once('done', function (signature) {
 | |
|         callback(null, signature);
 | |
|       });
 | |
|   } else {
 | |
|     return jws.sign({ header: header, payload: payload, secret: secretOrPrivateKey, encoding: encoding });
 | |
|   }
 | |
| };
 |