Initial migration to JWT

This commit is contained in:
Wyatt Johnson
2017-05-05 15:42:34 -06:00
parent d0cca476e6
commit 95f9bac254
12 changed files with 138 additions and 212 deletions
+5 -32
View File
@@ -3,12 +3,11 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const authentication = require('./middleware/authentication');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const enabled = require('debug').enabled;
const csrf = require('csurf');
const errors = require('./errors');
const session = require('./services/session');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
@@ -37,12 +36,6 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
app.use(session);
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
@@ -60,7 +53,10 @@ plugins.get('server', 'passport').forEach((plugin) => {
// Setup the PassportJS Middleware.
app.use(passport.initialize());
app.use(passport.session());
// Attach the authentication middleware, this will be responsible for decoding
// (if present) the JWT on the request.
app.use('/api', authentication);
//==============================================================================
// GraphQL Router
@@ -84,29 +80,6 @@ if (app.get('env') !== 'production') {
}
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
if (process.env.TEST_MODE === 'unit') {
// Add this fake test token in the event we are in unit test mode, and don't
// include the CSRF protection.
app.locals.csrfToken = 'UNIT_TESTS';
} else {
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
app.use(csrf({}));
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
}
//==============================================================================
// ROUTES
//==============================================================================
+19
View File
@@ -0,0 +1,19 @@
const {passport} = require('../services/passport');
const authentication = (req, res, next) => passport.authenticate('jwt', {
session: false
}, (err, user) => {
if (err) {
return next(err);
}
if (user) {
// Attach the user to the request object, now that we know it exists.
req.user = user;
}
next();
})(req, res, next);
module.exports = authentication;
+9 -12
View File
@@ -58,14 +58,12 @@
"commander": "^2.9.0",
"connect-redis": "^3.1.0",
"cross-spawn": "^5.1.0",
"csurf": "^1.9.0",
"dataloader": "^1.3.0",
"debug": "^2.6.3",
"dotenv": "^4.0.0",
"ejs": "^2.5.6",
"env-rewrite": "^1.0.2",
"express": "^4.15.2",
"express-session": "^1.15.1",
"form-data": "^2.1.2",
"gql-merge": "^0.0.4",
"graphql": "^0.9.1",
@@ -77,11 +75,10 @@
"helmet": "^3.5.0",
"inquirer": "^3.0.6",
"joi": "^10.4.1",
"jsonwebtoken": "^7.3.0",
"jsonwebtoken": "^7.4.0",
"kue": "^0.11.5",
"linkify-it": "^2.0.3",
"lodash": "^4.16.6",
"marked": "^0.3.6",
"metascraper": "^1.0.6",
"minimist": "^1.2.0",
"mongoose": "^4.9.1",
@@ -92,18 +89,12 @@
"nodemailer": "^2.6.4",
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
"prop-types": "^15.5.8",
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"recompose": "^0.23.1",
"redis": "^2.7.1",
"uuid": "^3.0.1",
"simplemde": "^1.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"uuid": "^3.0.1"
},
"devDependencies": {
@@ -157,6 +148,7 @@
"keymaster": "^1.6.2",
"license-webpack-plugin": "^0.4.2",
"material-design-lite": "^1.2.1",
"marked": "^0.3.6",
"mocha": "^3.1.2",
"mocha-junit-reporter": "^1.12.1",
"nightwatch": "^0.9.11",
@@ -178,11 +170,16 @@
"react-redux": "^4.4.5",
"react-router": "^3.0.0",
"react-tagsinput": "^3.14.0",
"prop-types": "^15.5.8",
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"recompose": "^0.23.1",
"redux": "^3.6.0",
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"selenium-standalone": "^5.11.2",
"simplemde": "^1.11.2",
"style-loader": "^0.16.0",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"supertest": "^2.0.1",
@@ -15,6 +15,6 @@ module.exports = (router) => {
router.get('/api/v1/auth/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
passport.authenticate('facebook', {session: false}, HandleAuthPopupCallback(req, res, next))(req, res, next);
});
};
+2 -12
View File
@@ -1,6 +1,5 @@
const express = require('express');
const {passport, HandleAuthCallback} = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const {passport, HandleGenerateCredentials} = require('../../../services/passport');
const router = express.Router();
@@ -20,15 +19,6 @@ router.get('/', (req, res, next) => {
res.json({user: req.user});
});
/**
* This destroys the session of a user, if they have one.
*/
router.delete('/', authorization.needed(), (req, res) => {
delete req.session.passport;
res.status(204).end();
});
//==============================================================================
// PASSPORT ROUTES
//==============================================================================
@@ -39,7 +29,7 @@ router.delete('/', authorization.needed(), (req, res) => {
router.post('/local', (req, res, next) => {
// Perform the local authentication.
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
passport.authenticate('local', {session: false}, HandleGenerateCredentials(req, res, next))(req, res, next);
});
module.exports = router;
+81 -41
View File
@@ -3,33 +3,43 @@ const UsersService = require('./users');
const SettingsService = require('./settings');
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');
const debug = require('debug')('talk:passport');
//==============================================================================
// SESSION SERIALIZATION
//==============================================================================
// JWT_SECRET is the secret used to sign and verify tokens issued by this
// application.
const JWT_SECRET = process.env.JWT_SECRET;
passport.serializeUser((user, done) => {
done(null, user.id);
// JWT_EXPIRY is the time for which a given token is valid for.
const JWT_EXPIRY = process.env.JWT_EXPIRY || '1 day';
// JWT_ISSUER is the value for the issuer for the tokens that will be verified
// when decoding. If `JWT_ISSUER` is not in the environment, then it will try
// `TALK_ROOT_URL`, otherwise, it will be undefined.
const JWT_ISSUER = process.env.JWT_ISSUER || process.env.TALK_ROOT_URL || undefined;
// 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`.
const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'talk';
// 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
});
passport.deserializeUser((id, done) => {
UsersService
.findById(id)
.then((user) => {
done(null, user);
})
.catch((err) => {
done(err);
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
// HandleGenerateCredentials validates that an authentication scheme did indeed
// return a user, if it did, then sign and return the user and token to be used
// by the frontend to display and update the UI.
const HandleGenerateCredentials = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
@@ -38,15 +48,11 @@ const HandleAuthCallback = (req, res, next) => (err, user) => {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// Generate the token to re-issue to the frontend.
const token = GenerateToken(user);
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
// Send back the details!
res.json({user, token});
};
/**
@@ -54,22 +60,18 @@ const HandleAuthCallback = (req, res, next) => (err, user) => {
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
return res.render('auth-callback', {auth: JSON.stringify({err, data: null})});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
return res.render('auth-callback', {auth: JSON.stringify({err, data: null})});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// Generate the token to re-issue to the frontend.
const token = GenerateToken(user);
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {auth: JSON.stringify({err: null, data: {user, token}})});
};
/**
@@ -119,7 +121,45 @@ function ValidateUserLogin(loginProfile, user, done) {
}
//==============================================================================
// STRATEGIES
// JWT STRATEGY
//==============================================================================
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
// Extract the JWT from the 'Authorization' header with the 'Bearer' scheme.
passport.use(new JwtStrategy({
// Prepare the extractor from the header.
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
// Use the secret passed in which is loaded from the environment. This can be
// a certificate (loaded) or a HMAC key.
secretOrKey: JWT_SECRET,
// Verify the issuer.
issuer: JWT_ISSUER,
// Verify the audience.
audience: JWT_AUDIENCE,
// Enable only the HS256 algorithm.
algorithms: ['HS256']
}, async (jwt, done) => {
// Load the user from the environment, because we just got a user from the
// header.
try {
let user = await UsersService.findById(jwt.sub);
return done(null, user);
} catch(e) {
return done(e);
}
}));
//==============================================================================
// LOCAL STRATEGY
//==============================================================================
/**
@@ -356,6 +396,6 @@ module.exports = {
passport,
ValidateUserLogin,
HandleFailedAttempt,
HandleAuthCallback,
HandleAuthPopupCallback
HandleAuthPopupCallback,
HandleGenerateCredentials
};
-36
View File
@@ -1,36 +0,0 @@
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./redis');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
client: redis.createClient(),
})
};
if (process.env.NODE_ENV === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
} else if (process.env.NODE_ENV === 'test') {
// Add in the secret during tests.
session_opts.secret = 'keyboard cat';
}
module.exports = session(session_opts);
+8 -2
View File
@@ -1,12 +1,18 @@
const session = require('./session');
const passport = require('./passport');
const authentication = require('../middleware/authentication');
// Session data does not automatically attach to websocket req objects.
// This middleware code looks for a user in the session and, if it exists,
// attaches it to the graph req.
const deserializeUser = (req) => {
return new Promise((resolve, reject) => {
session(req, {}, () => {
// This uses the authentication connect middleware to establish the session
// user details from the headers.
authentication(req, null, (err) => {
if (err) {
return reject(err);
}
if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) {
passport.deserializeUser(req.session.passport.user, (err, user) => {
-1
View File
@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<meta property="csrf" content="<%= csrfToken %>">
<title>Talk - Coral Admin</title>
<link rel="apple-touch-icon" sizes="57x57" href="/public/img/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/public/img/apple-icon-60x60.png">
+4 -1
View File
@@ -2,7 +2,10 @@
<html>
<body>
<script type="text/javascript">
window.opener.authCallback(<% if (err) { %>'<%- err %>'<% } else { %>null<% } %>, '<%- data %>');
<%/* set the auth data in localStorage, this will ensure that only
javascript on the same domain can access the data, they can listen
for updates by attaching to localStorage event changes */%>
localStorage.setItem('auth', <%- auth %>);
setTimeout(function() { window.close(); }, 50);
</script>
</body>
-1
View File
@@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta property="csrf" content="<%= csrfToken %>">
<link rel="stylesheet" type="text/css" href="/client/embed/stream/default.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
+9 -73
View File
@@ -1957,10 +1957,6 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
os-homedir "^1.0.1"
require-from-string "^1.1.0"
crc@3.4.4:
version "3.4.4"
resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b"
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -2024,14 +2020,6 @@ crypto-browserify@^3.11.0:
public-encrypt "^4.0.0"
randombytes "^2.0.0"
csrf@~3.0.3:
version "3.0.6"
resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a"
dependencies:
rndm "1.2.0"
tsscmp "1.0.5"
uid-safe "2.1.4"
css-color-function@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc"
@@ -2164,15 +2152,6 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0":
dependencies:
cssom "0.3.x"
csurf@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/csurf/-/csurf-1.9.0.tgz#49d2c6925ffcec7b7de559597c153fa533364133"
dependencies:
cookie "0.3.1"
cookie-signature "1.0.6"
csrf "~3.0.3"
http-errors "~1.5.0"
cz-conventional-changelog@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.1.5.tgz#0a4d1550c4e2fb6a3aed8f6cd858c21760e119b8"
@@ -2256,12 +2235,6 @@ debug@2.6.1:
dependencies:
ms "0.7.2"
debug@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d"
dependencies:
ms "0.7.2"
debug@~0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
@@ -2919,20 +2892,6 @@ exports-loader@^0.6.4:
loader-utils "^1.0.2"
source-map "0.5.x"
express-session@^1.15.1:
version "1.15.2"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.2.tgz#d98516443a4ccb8688e1725ae584c02daa4093d4"
dependencies:
cookie "0.3.1"
cookie-signature "1.0.6"
crc "3.4.4"
debug "2.6.3"
depd "~1.1.0"
on-headers "~1.0.1"
parseurl "~1.3.1"
uid-safe "~2.1.4"
utils-merge "1.0.0"
express@^4.12.2, express@^4.15.2:
version "4.15.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35"
@@ -3807,14 +3766,6 @@ htmlparser2@~3.8.1:
entities "1.0"
readable-stream "1.1"
http-errors@~1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750"
dependencies:
inherits "2.0.3"
setprototypeof "1.0.2"
statuses ">= 1.3.1 < 2"
http-errors@~1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
@@ -4547,7 +4498,7 @@ jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsonwebtoken@^7.3.0:
jsonwebtoken@^7.0.0, jsonwebtoken@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.0.tgz#515bf2bba070ec615bad97fd2e945027eb476946"
dependencies:
@@ -5817,13 +5768,20 @@ parseurl@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
passport-jwt@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-2.2.1.tgz#0e004c94071319d673d9d9bcfd1574a868011527"
dependencies:
jsonwebtoken "^7.0.0"
passport-strategy "^1.0.0"
passport-local@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
dependencies:
passport-strategy "1.x.x"
passport-strategy@1.x.x:
passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
@@ -6678,10 +6636,6 @@ ramda@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.23.0.tgz#ccd13fff73497a93974e3e86327bfd87bd6e8e2b"
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
randomatic@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"
@@ -7247,10 +7201,6 @@ ripemd160@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
rndm@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c"
run-async@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@@ -7364,10 +7314,6 @@ setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
setprototypeof@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
setprototypeof@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
@@ -7987,10 +7933,6 @@ tryor@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b"
tsscmp@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -8063,12 +8005,6 @@ uid-number@~0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
uid-safe@2.1.4, uid-safe@~2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81"
dependencies:
random-bytes "~1.0.0"
ultron@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"