Files
talk/services/jwt.js
T
2018-02-16 15:26:33 -07:00

155 lines
3.6 KiB
JavaScript

const jwt = require('jsonwebtoken');
const { merge, uniq, omitBy, isUndefined } = require('lodash');
/**
* MultiSecret will take many secrets and provide a unified interface for
* handling verifying and signing.
*/
class MultiSecret {
constructor(secrets) {
this.kids = secrets.map(({ kid }) => kid);
if (uniq(this.kids).length !== secrets.length) {
throw new Error(
"Duplicate kid's cannot be used to construct a MultiSecret"
);
}
this.secrets = secrets;
}
/**
* Sign will sign with the first secret.
*/
sign(payload, options) {
return this.secrets[0].sign(
omitBy(payload, isUndefined),
omitBy(options, isUndefined)
);
}
/**
* Verify will parse the token and determine the kid, then match it to the
* available secrets, using that to perform the verification.
*/
verify(token, options, callback) {
let header = null;
try {
header = JSON.parse(Buffer(token.split('.')[0], 'base64').toString());
} catch (err) {
return callback(err);
}
if (!('kid' in header)) {
return callback(
new Error('expected kid to exist in the token header, it did not.')
);
}
let kid = header.kid;
let verifier = this.secrets.find(secret => secret.kid === kid);
if (!verifier) {
return callback(new Error(`expected kid ${kid} was not available.`));
}
return verifier.verify(token, options, callback);
}
}
/**
* Secret wraps the capabilities expected of a Secret, signing and verifying.
*/
class Secret {
constructor({ kid, signingKey, verifiyingKey, algorithm }) {
this.kid = kid;
this.signingKey = signingKey;
this.verifiyingKey = verifiyingKey;
this.algorithm = algorithm;
}
/**
* Sign will sign the payload with the secret.
*
* @param {Object} payload the object to sign
* @param {Object} options the signing options
*/
sign(payload, options) {
if (!this.signingKey) {
throw new Error('no signing key on secret, cannot sign');
}
return jwt.sign(
payload,
this.signingKey,
omitBy(
merge({}, options, {
keyid: this.kid,
algorithm: this.algorithm,
}),
isUndefined
)
);
}
/**
* Verify will ensure that the given token was indeed signed with this secret.
* @param {String} token the token to verify
* @param {Object} options the verification options
* @param {Function} callback the function to call with the verification results
*/
verify(token, options, callback) {
jwt.verify(
token,
this.verifiyingKey,
Object.assign({}, options, {
algorithms: [this.algorithm],
}),
callback
);
}
}
/**
* SharedSecret is the HMAC based secret that's used for signing/verifying.
*/
function SharedSecret({ kid = undefined, secret = null }, algorithm) {
if (secret === null || secret.length === 0) {
throw new Error('Secret cannot have a zero length');
}
return new Secret({
kid,
signingKey: secret,
verifiyingKey: secret,
algorithm,
});
}
/**
* AsymmetricSecret is the Asymmetric based key, where a private key is optional
* and the public key is required.
*/
function AsymmetricSecret(
{ kid = undefined, private: privateKey, public: publicKey },
algorithm
) {
publicKey = Buffer.from(publicKey.replace(/\\n/g, '\n'));
privateKey =
privateKey && privateKey.length > 0
? Buffer.from(privateKey.replace(/\\n/g, '\n'))
: null;
return new Secret({
kid,
signingKey: privateKey,
verifiyingKey: publicKey,
algorithm,
});
}
module.exports = {
AsymmetricSecret,
SharedSecret,
MultiSecret,
};