mirror of
https://github.com/wassname/talk.git
synced 2026-07-19 11:28:50 +08:00
Expanded JWT Capabilities
This commit is contained in:
+2
-2
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user