Expanded JWT Capabilities

This commit is contained in:
Wyatt Johnson
2017-07-26 15:24:16 +10:00
parent f22350c92e
commit a91624bf4b
7 changed files with 147 additions and 16 deletions
+2 -2
View File
@@ -90,7 +90,7 @@ async function onListening() {
let bind = typeof addr === 'string'
? `pipe ${addr}`
: `port ${addr.port}`;
console.log(`API Server Listening on ${bind}`);
debug(`API Server Listening on ${bind}`);
}
/**
@@ -149,7 +149,7 @@ async function startApp(program) {
// Mount the websocket server if requested.
if (program.websockets) {
console.log(`Websocket Server Listening on ${port}`);
debug(`Websocket Server Listening on ${port}`);
// Mount the subscriptions server on the application server.
createSubscriptionManager(server);
+50 -1
View File
@@ -7,6 +7,8 @@
// entrypoint for the entire applications configuration.
require('env-rewrite').rewrite();
const debug = require('debug')('talk:config');
//==============================================================================
// CONFIG INITIALIZATION
//==============================================================================
@@ -24,6 +26,8 @@ const CONFIG = {
// application.
JWT_SECRET: process.env.TALK_JWT_SECRET || null,
JWT_SECRETS: process.env.TALK_JWT_SECRETS || null,
// JWT_AUDIENCE is the value for the audience claim for the tokens that will be
// verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it
// will default to `talk`.
@@ -102,8 +106,53 @@ const CONFIG = {
// JWT based configuration
//------------------------------------------------------------------------------
const jwt = require('./services/jwt');
if (CONFIG.JWT_SECRETS) {
CONFIG.JWT_SECRETS = JSON.parse(CONFIG.JWT_SECRETS);
if (!Array.isArray(CONFIG.JWT_SECRETS)) {
throw new Error('TALK_JWT_SECRETS must be a JSON array in the form [{"kid": kid, ["secret": secret | "private": private, "public": public]}, ...]');
}
if (CONFIG.JWT_SECRETS.length === 0) {
throw new Error('TALK_JWT_SECRETS must be a JSON array with non zero length');
}
// Wrap a multi-secret around the available secrets.
CONFIG.JWT_SECRET = new jwt.MultiSecret(CONFIG.JWT_SECRETS.map((secret) => {
if (!('kid' in secret)) {
throw new Error('when multiple keys are specified, kid\'s must be specified');
}
// HMAC secrets do not have public/private keys.
if (CONFIG.JWT_ALG.startsWith('HS')) {
return new jwt.SharedSecret(secret);
}
if (!('public' in secret)) {
throw new Error('all symetric keys must provide a PEM encoded public key');
}
return new jwt.AsymmetricSecret(secret);
}));
debug(`loaded ${CONFIG.JWT_SECRET.length} secrets`);
} else if (CONFIG.JWT_SECRET) {
if (CONFIG.JWT_ALG.startsWith('HS')) {
CONFIG.JWT_SECRET = new jwt.SharedSecret({
secret: CONFIG.JWT_SECRET
});
} else {
CONFIG.JWT_SECRET = new jwt.AsymmetricSecret(JSON.parse(CONFIG.JWT_SECRET));
}
debug('loaded 1 secret');
}
if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) {
CONFIG.JWT_SECRET = 'keyboard cat';
CONFIG.JWT_SECRET = new jwt.SharedSecret({
secret: 'keyboard cat'
});
} else if (!CONFIG.JWT_SECRET) {
throw new Error(
'TALK_JWT_SECRET must be provided in the environment to sign/verify tokens'
+81
View File
@@ -0,0 +1,81 @@
const jwt = require('jsonwebtoken');
/**
* MultiSecret will take many secrets and provide a unified interface for
* handling verifying and signing.
*/
class MultiSecret {
constructor(secrets) {
this.secrets = secrets;
}
sign(payload, options) {
return this.secrets[0].sign(payload, options);
}
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);
}
}
class SharedSecret {
constructor({kid = undefined, secret}) {
this.kid = kid;
this.secret = secret;
}
sign(payload, options) {
return jwt.sign(payload, this.secret, Object.assign({}, options, {
keyid: this.kid
}));
}
verify(token, options, callback) {
jwt.verify(token, this.secret, options, callback);
}
}
class AsymmetricSecret {
constructor({kid = undefined, private: privateKey, public: publicKey}) {
this.kid = kid;
this.public = Buffer.from(publicKey.replace(/\\n/g, '\n'));
this.private = privateKey ? Buffer.from(privateKey.replace(/\\n/g, '\n')) : null;
}
sign(payload, options) {
if (!this.private) {
throw new Error('no private key on secret, cannot sign');
}
return jwt.sign(payload, this.private, Object.assign({}, options, {
keyid: this.kid
}));
}
verify(token, options, callback) {
jwt.verify(token, this.public, options, callback);
}
}
module.exports = {
AsymmetricSecret,
SharedSecret,
MultiSecret
};
+1 -1
View File
@@ -43,7 +43,7 @@ if (enabled('talk:db')) {
if (WEBPACK) {
console.warn('Not connecting to mongodb during webpack build');
debug('Not connecting to mongodb during webpack build');
// @wyattjoh: We didn't call connect, but because we include mongoose, it will hold the socket ready,
// preventing node from exiting. Calling disconnect here just ensures that the application
+11 -10
View File
@@ -4,7 +4,6 @@ const SettingsService = require('./settings');
const TokensService = require('./tokens');
const fetch = require('node-fetch');
const FormData = require('form-data');
const JWT = require('jsonwebtoken');
const LocalStrategy = require('passport-local').Strategy;
const errors = require('../errors');
const uuid = require('uuid');
@@ -28,13 +27,16 @@ const {
// GenerateToken will sign a token to include all the authorization information
// needed for the front end.
const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, {
jwtid: uuid.v4(),
expiresIn: JWT_EXPIRY,
issuer: JWT_ISSUER,
subject: user.id,
audience: JWT_AUDIENCE
});
const GenerateToken = (user) => {
return JWT_SECRET.sign({}, {
jwtid: uuid.v4(),
expiresIn: JWT_EXPIRY,
issuer: JWT_ISSUER,
subject: user.id,
audience: JWT_AUDIENCE,
algorithm: JWT_ALG
});
};
// SetTokenForSafari sends the token in a cookie for Safari clients.
const SetTokenForSafari = (req, res, token) => {
@@ -187,7 +189,6 @@ const CheckBlacklisted = async (jwt) => {
return checkGeneralTokenBlacklist(jwt);
};
const jwt = require('jsonwebtoken');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
@@ -204,7 +205,7 @@ let cookieExtractor = function(req) {
// Override the JwtVerifier method on the JwtStrategy so we can pack the
// original token into the payload.
JwtStrategy.JwtVerifier = (token, secretOrKey, options, callback) => {
return jwt.verify(token, secretOrKey, options, (err, jwt) => {
return JWT_SECRET.verify(token, options, (err, jwt) => {
if (err) {
return callback(err);
}
-1
View File
@@ -37,7 +37,6 @@ describe('graph.mutations.addTag', () => {
console.error(res.errors);
}
console.log('res.errors', res.errors);
expect(res.errors).to.be.empty;
let {tags} = await CommentsService.findById(comment.id);
+2 -1
View File
@@ -7,6 +7,7 @@ const _ = require('lodash');
const Copy = require('copy-webpack-plugin');
const LicenseWebpackPlugin = require('license-webpack-plugin');
const webpack = require('webpack');
const debug = require('debug')('talk:webpack');
// Possibly load the config from the .env file (if there is one).
require('dotenv').config();
@@ -15,7 +16,7 @@ const {plugins, pluginsPath, PluginManager} = require('./plugins');
const manager = new PluginManager(plugins);
const targetPlugins = manager.section('targets').plugins;
console.log(`Using ${pluginsPath} as the plugin configuration path`);
debug(`Using ${pluginsPath} as the plugin configuration path`);
const buildTargets = [
'coral-admin',