From 95f9bac2548cb0fd93f66122b63ade0e8c3c2d42 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 5 May 2017 15:42:34 -0600 Subject: [PATCH 01/18] Initial migration to JWT --- app.js | 37 +----- middleware/authentication.js | 19 +++ package.json | 21 ++- .../server/router.js | 2 +- routes/api/auth/index.js | 14 +- services/passport.js | 122 ++++++++++++------ services/session.js | 36 ------ services/subscriptions.js | 10 +- views/admin.ejs | 1 - views/auth-callback.ejs | 5 +- views/embed/stream.ejs | 1 - yarn.lock | 82 ++---------- 12 files changed, 138 insertions(+), 212 deletions(-) create mode 100644 middleware/authentication.js delete mode 100644 services/session.js diff --git a/app.js b/app.js index a0a5b7789..f5243f81a 100644 --- a/app.js +++ b/app.js @@ -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 //============================================================================== diff --git a/middleware/authentication.js b/middleware/authentication.js new file mode 100644 index 000000000..ca6949a5c --- /dev/null +++ b/middleware/authentication.js @@ -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; diff --git a/package.json b/package.json index bfca21643..f5ab201ef 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plugins/coral-plugin-facebook-auth/server/router.js b/plugins/coral-plugin-facebook-auth/server/router.js index 7ef7b0ed2..56fcd2ab2 100644 --- a/plugins/coral-plugin-facebook-auth/server/router.js +++ b/plugins/coral-plugin-facebook-auth/server/router.js @@ -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); }); }; diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 4926192de..54932ac03 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -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; diff --git a/services/passport.js b/services/passport.js index 682d86ce1..a455fa745 100644 --- a/services/passport.js +++ b/services/passport.js @@ -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 }; diff --git a/services/session.js b/services/session.js deleted file mode 100644 index 505dfdf4a..000000000 --- a/services/session.js +++ /dev/null @@ -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); diff --git a/services/subscriptions.js b/services/subscriptions.js index 8c34507f2..c4cd2378a 100644 --- a/services/subscriptions.js +++ b/services/subscriptions.js @@ -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) => { diff --git a/views/admin.ejs b/views/admin.ejs index 947f15425..946284622 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -3,7 +3,6 @@ - Talk - Coral Admin diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs index d38d759ee..a97884ae2 100644 --- a/views/auth-callback.ejs +++ b/views/auth-callback.ejs @@ -2,7 +2,10 @@ diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index d99d4bce1..03897d85f 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -3,7 +3,6 @@ - diff --git a/yarn.lock b/yarn.lock index 99703bdf3..4663f70fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 76d0fdfd6767ba6425042a509fd3973599842cb8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 15:32:19 -0600 Subject: [PATCH 02/18] Added docs --- README.md | 7 +++++-- services/passport.js | 5 ++++- services/users.js | 18 +++++++++--------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 74097d48c..ec952b321 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,13 @@ The Talk application looks for the following configuration values either as envi - `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database. - `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database. -- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to -secure cookies. - `TALK_ROOT_URL` (*required*) - root url of the installed application externally available in the format: `://` without the path. +- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to +sign and verify tokens via a `HS256` algorithm. +- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`) +- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`) +- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`) - `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the SMTP provider. - `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using. diff --git a/services/passport.js b/services/passport.js index a455fa745..41d57a539 100644 --- a/services/passport.js +++ b/services/passport.js @@ -11,7 +11,10 @@ const debug = require('debug')('talk:passport'); // JWT_SECRET is the secret used to sign and verify tokens issued by this // application. -const JWT_SECRET = process.env.JWT_SECRET; +const JWT_SECRET = process.env.JWT_SECRET || null; +if (JWT_SECRET === null) { + throw new Error('JWT_SECRET must be provided in the environment to sign/verify tokens'); +} // JWT_EXPIRY is the time for which a given token is valid for. const JWT_EXPIRY = process.env.JWT_EXPIRY || '1 day'; diff --git a/services/users.js b/services/users.js index 14301ccf4..59cc6ba7d 100644 --- a/services/users.js +++ b/services/users.js @@ -22,12 +22,12 @@ const SettingsService = require('./settings'); const ActionsService = require('./actions'); const MailerService = require('./mailer'); -// In the event that the TALK_SESSION_SECRET is missing but we are testing, then -// set the process.env.TALK_SESSION_SECRET. -if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) { - process.env.TALK_SESSION_SECRET = 'keyboard cat'; -} else if (!process.env.TALK_SESSION_SECRET) { - throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality'); +// In the event that the TALK_JWT_SECRET is missing but we are testing, then +// set the process.env.TALK_JWT_SECRET. +if (process.env.NODE_ENV === 'test' && !process.env.TALK_JWT_SECRET) { + process.env.TALK_JWT_SECRET = 'keyboard cat'; +} else if (!process.env.TALK_JWT_SECRET) { + throw new Error('TALK_JWT_SECRET must be defined to encode JSON Web Tokens and other auth functionality'); } const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm'; @@ -564,7 +564,7 @@ module.exports = class UsersService { version: user.__v }; - return jwt.sign(payload, process.env.TALK_SESSION_SECRET, { + return jwt.sign(payload, process.env.TALK_JWT_SECRET, { algorithm: 'HS256', expiresIn: '1d', subject: PASSWORD_RESET_JWT_SUBJECT @@ -583,7 +583,7 @@ module.exports = class UsersService { // Set the allowed algorithms. options.algorithms = ['HS256']; - jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => { + jwt.verify(token, process.env.TALK_JWT_SECRET, options, (err, decoded) => { if (err) { return reject(err); } @@ -740,7 +740,7 @@ module.exports = class UsersService { email, referer, userID: user.id - }, process.env.TALK_SESSION_SECRET, tokenOptions); + }, process.env.TALK_JWT_SECRET, tokenOptions); }); } From 785e5a4ac8bccfa9ea8e822150b091f7857182c3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 16:49:11 -0600 Subject: [PATCH 03/18] Collected config --- bin/cli-serve | 6 +- bin/commander.js | 5 -- config.js | 147 ++++++++++++++++++++++++++++++++++++ plugins.js | 5 +- routes/admin/index.js | 5 +- routes/api/account/index.js | 5 +- routes/api/users/index.js | 5 +- routes/embed/index.js | 5 +- services/mailer.js | 29 ++++--- services/mongoose.js | 19 ++--- services/passport.js | 42 ++--------- services/redis.js | 6 +- services/setup.js | 5 +- services/users.js | 24 +++--- webpack.config.js | 7 +- 15 files changed, 221 insertions(+), 94 deletions(-) create mode 100644 config.js diff --git a/bin/cli-serve b/bin/cli-serve index eca23303b..7978c6c85 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -9,13 +9,15 @@ const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); const {createSubscriptionManager} = require('../graph/subscriptions'); +const { + PORT +} = require('../config'); /** * Get port from environment and store in Express. */ -const port = normalizePort(process.env.TALK_PORT || '3000'); - +const port = normalizePort(PORT); app.set('port', port); /** diff --git a/bin/commander.js b/bin/commander.js index ad346a911..328a25b29 100644 --- a/bin/commander.js +++ b/bin/commander.js @@ -3,11 +3,6 @@ const dotenv = require('dotenv'); const fs = require('fs'); const program = require('commander'); -// Perform rewrites to the runtime environment variables based on the contents -// of the process.env.REWRITE_ENV if it exists. This is done here as it is the -// entrypoint for the entire application. -require('env-rewrite').rewrite(); - //============================================================================== // Setting up the program command line arguments. //============================================================================== diff --git a/config.js b/config.js new file mode 100644 index 000000000..271f13420 --- /dev/null +++ b/config.js @@ -0,0 +1,147 @@ +// This file serves as the entrypoint to all configuration loaded by the +// application. All defaults are assumed here, validation should also be +// completed here. + +// Perform rewrites to the runtime environment variables based on the contents +// of the process.env.REWRITE_ENV if it exists. This is done here as it is the +// entrypoint for the entire applications configuration. +require('env-rewrite').rewrite(); + +//============================================================================== +// CONFIG INITIALIZATION +//============================================================================== + +const CONFIG = { + + //------------------------------------------------------------------------------ + // JWT based configuration + //------------------------------------------------------------------------------ + + // JWT_SECRET is the secret used to sign and verify tokens issued by this + // application. + JWT_SECRET: process.env.TALK_JWT_SECRET || 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`. + JWT_AUDIENCE: process.env.TALK_JWT_AUDIENCE || 'talk', + + // 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. + JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL || undefined, + + // JWT_EXPIRY is the time for which a given token is valid for. + JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day', + + //------------------------------------------------------------------------------ + // Installation locks + //------------------------------------------------------------------------------ + + INSTALL_LOCK: process.env.TALK_INSTALL_LOCK === 'TRUE', + + //------------------------------------------------------------------------------ + // External database url's + //------------------------------------------------------------------------------ + + MONGO_URL: process.env.TALK_MONGO_URL, + REDIS_URL: process.env.TALK_REDIS_URL, + + //------------------------------------------------------------------------------ + // Plugins + //------------------------------------------------------------------------------ + + PLUGINS_JSON: process.env.TALK_PLUGINS_JSON, + + //------------------------------------------------------------------------------ + // Server Config + //------------------------------------------------------------------------------ + + // Port to bind to. + PORT: process.env.TALK_PORT || '3000', + + // The URL for this Talk Instance as viewable from the outside. + ROOT_URL: process.env.TALK_ROOT_URL, + + //------------------------------------------------------------------------------ + // Recaptcha configuration + //------------------------------------------------------------------------------ + + RECAPTCHA_ENABLED: false, // updated below + RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC, + RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET, + + //------------------------------------------------------------------------------ + // SMTP Server configuration + //------------------------------------------------------------------------------ + + SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS, + SMTP_HOST: process.env.TALK_SMTP_HOST, + SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD, + SMTP_PORT: process.env.TALK_SMTP_PORT, + SMTP_USERNAME: process.env.TALK_SMTP_USERNAME +}; + +//============================================================================== +// CONFIG VALIDATION +//============================================================================== + +//------------------------------------------------------------------------------ +// JWT based configuration +//------------------------------------------------------------------------------ + +if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) { + CONFIG.JWT_SECRET = 'keyboard cat'; +} else if (!CONFIG.JWT_SECRET) { + throw new Error('TALK_JWT_SECRET must be provided in the environment to sign/verify tokens'); +} + +//------------------------------------------------------------------------------ +// External database url's +//------------------------------------------------------------------------------ + +// Reset the mongo url in the event it hasn't been overrided and we are in a +// testing environment. Every new mongo instance comes with a test database by +// default, this is consistent with common testing and use case practices. +if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) { + CONFIG.MONGO_URL = 'mongodb://localhost/test'; +} + +// Reset the redis url in the event it hasn't been overrided and we are in a +// testing environment. +if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) { + CONFIG.REDIS_URL = 'redis://localhost'; +} + +//------------------------------------------------------------------------------ +// Recaptcha configuration +//------------------------------------------------------------------------------ + +/** + * This is true when the recaptcha secret is provided and the Recaptcha feature + * is to be enabled. + */ +CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET && CONFIG.RECAPTCHA_SECRET.length > 0 && + CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0; +if (!CONFIG.RECAPTCHA_ENABLED) { + console.warn('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); +} + +//------------------------------------------------------------------------------ +// SMTP Server configuration +//------------------------------------------------------------------------------ + +{ + const requiredProps = [ + 'SMTP_FROM_ADDRESS', + 'SMTP_USERNAME', + 'SMTP_PASSWORD', + 'SMTP_HOST' + ]; + + if (requiredProps.some((prop) => !CONFIG[prop])) { + console.warn(`${requiredProps.map((v) => `TALK_${v}`).join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); + } +} + +module.exports = CONFIG; diff --git a/plugins.js b/plugins.js index 22293c975..f353cd392 100644 --- a/plugins.js +++ b/plugins.js @@ -4,6 +4,9 @@ const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); const Joi = require('joi'); const amp = require('app-module-path'); +const { + PLUGINS_JSON +} = require('./config'); // Add the current path to the module root. amp.addPath(__dirname); @@ -18,7 +21,7 @@ try { let customPlugins = path.join(__dirname, 'plugins.json'); let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { + if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); plugins = require(envPlugins); } else if (fs.existsSync(customPlugins)) { diff --git a/routes/admin/index.js b/routes/admin/index.js index 1a9769591..44ce83222 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,5 +1,8 @@ const express = require('express'); const router = express.Router(); +const { + RECAPTCHA_PUBLIC +} = require('../../config'); // Get /email-confirmation expects a signed JWT in the hash router.get('/confirm-email', (req, res) => { @@ -17,7 +20,7 @@ router.get('/password-reset', (req, res) => { router.get('*', (req, res) => { const data = { - TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC }; res.render('admin', {basePath: '/client/coral-admin', data}); diff --git a/routes/api/account/index.js b/routes/api/account/index.js index e1c73d638..4c49c00a5 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -4,6 +4,9 @@ const UsersService = require('../../../services/users'); const mailer = require('../../../services/mailer'); const authorization = require('../../../middleware/authorization'); const errors = require('../../../errors'); +const { + ROOT_URL +} = require('../../../config'); //============================================================================== // ROUTES @@ -62,7 +65,7 @@ router.post('/password/reset', (req, res, next) => { template: 'password-reset', // needed to know which template to render! locals: { // specifies the template locals. token, - rootURL: process.env.TALK_ROOT_URL + rootURL: ROOT_URL }, subject: 'Password Reset', to: email diff --git a/routes/api/users/index.js b/routes/api/users/index.js index a01fed2c9..9d81b87b7 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -5,6 +5,9 @@ const CommentsService = require('../../../services/comments'); const mailer = require('../../../services/mailer'); const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); +const { + ROOT_URL +} = require('../../../config'); // get a list of users. router.get('/', authorization.needed('ADMIN'), (req, res, next) => { @@ -108,7 +111,7 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService template: 'email-confirm', // needed to know which template to render! locals: { // specifies the template locals. token, - rootURL: process.env.TALK_ROOT_URL, + rootURL: ROOT_URL, email }, subject: 'Email Confirmation', diff --git a/routes/embed/index.js b/routes/embed/index.js index efa563422..0aaa9ff9c 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -1,6 +1,9 @@ const express = require('express'); const router = express.Router(); const SettingsService = require('../../services/settings'); +const { + RECAPTCHA_PUBLIC +} = require('../../config'); router.use('/:embed', (req, res, next) => { switch (req.params.embed) { @@ -8,7 +11,7 @@ router.use('/:embed', (req, res, next) => { return SettingsService.retrieve() .then(({customCssUrl}) => { const data = { - TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC }; return res.render('embed/stream', {customCssUrl, data}); diff --git a/services/mailer.js b/services/mailer.js index 35741ee9f..f3d9235ba 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -5,16 +5,13 @@ const path = require('path'); const fs = require('fs'); const _ = require('lodash'); -const smtpRequiredProps = [ - 'TALK_SMTP_FROM_ADDRESS', - 'TALK_SMTP_USERNAME', - 'TALK_SMTP_PASSWORD', - 'TALK_SMTP_HOST' -]; - -if (smtpRequiredProps.some(prop => !process.env[prop])) { - console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); -} +const { + SMTP_HOST, + SMTP_USERNAME, + SMTP_PORT, + SMTP_PASSWORD, + SMTP_FROM_ADDRESS +} = require('../config'); // load all the templates as strings const templates = { @@ -56,15 +53,15 @@ templates.render = (name, format = 'txt', context) => new Promise((resolve, reje }); const options = { - host: process.env.TALK_SMTP_HOST, + host: SMTP_HOST, auth: { - user: process.env.TALK_SMTP_USERNAME, - pass: process.env.TALK_SMTP_PASSWORD + user: SMTP_USERNAME, + pass: SMTP_PASSWORD } }; -if (process.env.TALK_SMTP_PORT) { - options.port = process.env.TALK_SMTP_PORT; +if (SMTP_PORT) { + options.port = SMTP_PORT; } else { options.port = 25; } @@ -126,7 +123,7 @@ const mailer = module.exports = { debug(`Starting to send mail for Job[${id}]`); // Set the `from` field. - data.message.from = process.env.TALK_SMTP_FROM_ADDRESS; + data.message.from = SMTP_FROM_ADDRESS; // Actually send the email. defaultTransporter.sendMail(data.message, (err) => { diff --git a/services/mongoose.js b/services/mongoose.js index c7092326f..acda2f564 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -1,7 +1,12 @@ const mongoose = require('mongoose'); const debug = require('debug')('talk:db'); +const enabled = require('debug').enabled; const queryDebuger = require('debug')('talk:db:query'); +const { + MONGO_URL +} = require('../config'); + // Loading the formatter from Mongoose: // // https://github.com/Automattic/mongoose/blob/1a93d1f4d12e441e17ddf451e96fbc5f6e8f54b8/lib/drivers/node-mongodb-native/collection.js#L182 @@ -24,18 +29,6 @@ function debugQuery(name, i, ...args) { queryDebuger(functionCall + params); } -const enabled = require('debug').enabled; - -// Pull the mongo url out of the environment. -let url = process.env.TALK_MONGO_URL; - -// Reset the mongo url in the event it hasn't been overrided and we are in a -// testing environment. Every new mongo instance comes with a test database by -// default, this is consistent with common testing and use case practices. -if (process.env.NODE_ENV === 'test' && !url) { - url = 'mongodb://localhost/test'; -} - // Use native promises mongoose.Promise = global.Promise; @@ -48,7 +41,7 @@ if (enabled('talk:db')) { } // Connect to the Mongo instance. -mongoose.connect(url, (err) => { +mongoose.connect(MONGO_URL, (err) => { if (err) { throw err; } diff --git a/services/passport.js b/services/passport.js index 41d57a539..fd56a8c86 100644 --- a/services/passport.js +++ b/services/passport.js @@ -9,25 +9,14 @@ const errors = require('../errors'); const uuid = require('uuid'); const debug = require('debug')('talk:passport'); -// JWT_SECRET is the secret used to sign and verify tokens issued by this -// application. -const JWT_SECRET = process.env.JWT_SECRET || null; -if (JWT_SECRET === null) { - throw new Error('JWT_SECRET must be provided in the environment to sign/verify tokens'); -} - -// 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'; +const { + JWT_SECRET, + JWT_ISSUER, + JWT_EXPIRY, + JWT_AUDIENCE, + RECAPTCHA_SECRET, + RECAPTCHA_ENABLED +} = require('../config'); // GenerateToken will sign a token to include all the authorization information // needed for the front end. @@ -200,21 +189,6 @@ const CheckIfNeedsRecaptcha = (user, email) => { return false; }; -/** - * This stores the Recaptcha secret. - */ -const RECAPTCHA_SECRET = process.env.TALK_RECAPTCHA_SECRET; -const RECAPTCHA_PUBLIC = process.env.TALK_RECAPTCHA_PUBLIC; - -/** - * This is true when the recaptcha secret is provided and the Recaptcha feature - * is to be enabled. - */ -const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0 && RECAPTCHA_PUBLIC && RECAPTCHA_PUBLIC.length > 0; -if (!RECAPTCHA_ENABLED) { - console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); -} - /** * This sends the request details down Google to check to see if the response is * genuine or not. diff --git a/services/redis.js b/services/redis.js index d27303fca..c049b2d00 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,9 +1,11 @@ const redis = require('redis'); const debug = require('debug')('talk:redis'); -const url = process.env.TALK_REDIS_URL || 'redis://localhost'; +const { + REDIS_URL +} = require('../config'); const connectionOptions = { - url, + url: REDIS_URL, retry_strategy: function(options) { if (options.error && options.error.code === 'ECONNREFUSED') { diff --git a/services/setup.js b/services/setup.js index 1f1542d5e..233021c17 100644 --- a/services/setup.js +++ b/services/setup.js @@ -2,6 +2,9 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); const SettingsModel = require('../models/setting'); const errors = require('../errors'); +const { + INSTALL_LOCK +} = require('../config'); /** * This service is used when we want to setup the application. It is consumed by @@ -15,7 +18,7 @@ module.exports = class SetupService { static isAvailable() { // Check if we have an install lock present. - if (process.env.TALK_INSTALL_LOCK === 'TRUE') { + if (INSTALL_LOCK) { return Promise.reject(errors.ErrInstallLock); } diff --git a/services/users.js b/services/users.js index 59cc6ba7d..03c1671d5 100644 --- a/services/users.js +++ b/services/users.js @@ -1,12 +1,14 @@ const assert = require('assert'); +const uuid = require('uuid'); const bcrypt = require('bcrypt'); const url = require('url'); const jwt = require('jsonwebtoken'); const Wordlist = require('./wordlist'); - const errors = require('../errors'); - -const uuid = require('uuid'); +const { + JWT_SECRET, + ROOT_URL +} = require('../config'); const redis = require('./redis'); const redisClient = redis.createClient(); @@ -22,14 +24,6 @@ const SettingsService = require('./settings'); const ActionsService = require('./actions'); const MailerService = require('./mailer'); -// In the event that the TALK_JWT_SECRET is missing but we are testing, then -// set the process.env.TALK_JWT_SECRET. -if (process.env.NODE_ENV === 'test' && !process.env.TALK_JWT_SECRET) { - process.env.TALK_JWT_SECRET = 'keyboard cat'; -} else if (!process.env.TALK_JWT_SECRET) { - throw new Error('TALK_JWT_SECRET must be defined to encode JSON Web Tokens and other auth functionality'); -} - const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm'; const PASSWORD_RESET_JWT_SUBJECT = 'password_reset'; @@ -564,7 +558,7 @@ module.exports = class UsersService { version: user.__v }; - return jwt.sign(payload, process.env.TALK_JWT_SECRET, { + return jwt.sign(payload, JWT_SECRET, { algorithm: 'HS256', expiresIn: '1d', subject: PASSWORD_RESET_JWT_SUBJECT @@ -583,7 +577,7 @@ module.exports = class UsersService { // Set the allowed algorithms. options.algorithms = ['HS256']; - jwt.verify(token, process.env.TALK_JWT_SECRET, options, (err, decoded) => { + jwt.verify(token, JWT_SECRET, options, (err, decoded) => { if (err) { return reject(err); } @@ -697,7 +691,7 @@ module.exports = class UsersService { * @param {String} email The email that we are needing to get confirmed. * @return {Promise} */ - static createEmailConfirmToken(userID = null, email, referer = process.env.TALK_ROOT_URL) { + static createEmailConfirmToken(userID = null, email, referer = ROOT_URL) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); } @@ -740,7 +734,7 @@ module.exports = class UsersService { email, referer, userID: user.id - }, process.env.TALK_JWT_SECRET, tokenOptions); + }, JWT_SECRET, tokenOptions); }); } diff --git a/webpack.config.js b/webpack.config.js index 3a54e85c0..5aade15a1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,13 +9,18 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); +// Load the config after dotenv has does it's thing. +const { + PLUGINS_JSON +} = require('./config'); + let pluginsConfigPath; let envPlugins = path.join(__dirname, 'plugins.env.js'); let customPlugins = path.join(__dirname, 'plugins.json'); let defaultPlugins = path.join(__dirname, 'plugins.default.json'); -if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { +if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { pluginsConfigPath = envPlugins; } else if (fs.existsSync(customPlugins)) { pluginsConfigPath = customPlugins; From 17b9e43c14c924168d731a96feabb9577e46abf6 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 9 May 2017 16:53:30 -0600 Subject: [PATCH 04/18] moved webpack config --- webpack.config.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 5aade15a1..3a54e85c0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,18 +9,13 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); -// Load the config after dotenv has does it's thing. -const { - PLUGINS_JSON -} = require('./config'); - let pluginsConfigPath; let envPlugins = path.join(__dirname, 'plugins.env.js'); let customPlugins = path.join(__dirname, 'plugins.json'); let defaultPlugins = path.join(__dirname, 'plugins.default.json'); -if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { +if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { pluginsConfigPath = envPlugins; } else if (fs.existsSync(customPlugins)) { pluginsConfigPath = customPlugins; From f8b207e563794ba74efafc5ebc78ee1f5a460d8e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 10 May 2017 10:29:20 -0600 Subject: [PATCH 05/18] Return a meaningful error --- services/passport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/passport.js b/services/passport.js index fd56a8c86..467605a1a 100644 --- a/services/passport.js +++ b/services/passport.js @@ -56,7 +56,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { } if (!user) { - return res.render('auth-callback', {auth: JSON.stringify({err, data: null})}); + return res.render('auth-callback', {auth: JSON.stringify({err: errors.ErrNotAuthorized, data: null})}); } // Generate the token to re-issue to the frontend. From 5e8a02c461d79a77beee702d1c00b6675d7d720f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 12 May 2017 15:40:53 -0300 Subject: [PATCH 06/18] Removing CSRF --- client/coral-admin/src/constants/auth.js | 2 -- client/coral-framework/constants/auth.js | 2 -- client/coral-framework/helpers/response.js | 13 ------------- client/coral-framework/reducers/auth.js | 3 --- 4 files changed, 20 deletions(-) diff --git a/client/coral-admin/src/constants/auth.js b/client/coral-admin/src/constants/auth.js index 93cd61544..610b21c89 100644 --- a/client/coral-admin/src/constants/auth.js +++ b/client/coral-admin/src/constants/auth.js @@ -2,8 +2,6 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST'; export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; -export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; - export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index ce5d860b4..b5d80e32f 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -44,8 +44,6 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST'; export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; -export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; - export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST'; export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS'; export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index 0fce878cd..05d515b7d 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -2,8 +2,6 @@ export const base = '/api/v1'; const buildOptions = (inputOptions = {}) => { - const csurfDOM = document.head.querySelector('[property=csrf]'); - const defaultOptions = { method: 'GET', headers: { @@ -11,22 +9,11 @@ const buildOptions = (inputOptions = {}) => { 'Accept': 'application/json' }, credentials: 'same-origin', - _csrf: csurfDOM ? csurfDOM.content : false }; let options = Object.assign({}, defaultOptions, inputOptions); options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers); - if (options._csrf) { - switch (options.method.toLowerCase()) { - case 'post': - case 'put': - case 'delete': - options.headers['x-csrf-token'] = options._csrf; - break; - } - } - if (options.method.toLowerCase() !== 'get') { options.body = JSON.stringify(options.body); } diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 83ea49ce5..dc68a442c 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -64,9 +64,6 @@ export default function auth (state = initialState, action) { .set('view', action.view); case actions.CLEAN_STATE: return initialState; - case actions.CHECK_CSRF_TOKEN: - return state - .set('_csrf', action._csrf); case actions.FETCH_SIGNIN_REQUEST: return state .set('isLoading', true); From 7fd55094901ab2398afc54ab2ece32d78283cb1d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 12 May 2017 15:48:19 -0300 Subject: [PATCH 07/18] Prettier --- client/coral-framework/actions/auth.js | 111 +++++++++++++++++-------- 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index d689aeb63..0f96a463e 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -28,7 +28,8 @@ const ME_QUERY = gql` function fetchMe() { return client.query({ fetchPolicy: 'network-only', - query: ME_QUERY}); + query: ME_QUERY + }); } // Dialog Actions @@ -61,14 +62,26 @@ export const hideSignInDialog = () => dispatch => { window.close(); }; -export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST}); -export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG}); -export const hideCreateUsernameDialog = () => ({type: actions.HIDE_CREATEUSERNAME_DIALOG}); +export const createUsernameRequest = () => ({ + type: actions.CREATE_USERNAME_REQUEST +}); +export const showCreateUsernameDialog = () => ({ + type: actions.SHOW_CREATEUSERNAME_DIALOG +}); +export const hideCreateUsernameDialog = () => ({ + type: actions.HIDE_CREATEUSERNAME_DIALOG +}); const createUsernameSuccess = () => ({type: actions.CREATE_USERNAME_SUCCESS}); -const createUsernameFailure = error => ({type: actions.CREATE_USERNAME_FAILURE, error}); +const createUsernameFailure = error => ({ + type: actions.CREATE_USERNAME_FAILURE, + error +}); -export const updateUsername = ({username}) => ({type: actions.UPDATE_USERNAME, username}); +export const updateUsername = ({username}) => ({ + type: actions.UPDATE_USERNAME, + username +}); export const createUsername = (userId, formData) => dispatch => { dispatch(createUsernameRequest()); @@ -89,15 +102,15 @@ export const changeView = view => dispatch => { view }); - switch(view) { - case 'SIGNUP': - window.resizeTo(500, 800); - break; - case 'FORGOT': - window.resizeTo(500, 400); - break; - default: - window.resizeTo(500, 550); + switch (view) { + case 'SIGNUP': + window.resizeTo(500, 800); + break; + case 'FORGOT': + window.resizeTo(500, 400); + break; + default: + window.resizeTo(500, 550); } }; @@ -112,17 +125,17 @@ const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); // const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); -export const fetchSignIn = (formData) => (dispatch) => { +export const fetchSignIn = formData => dispatch => { dispatch(signInRequest()); return coralApi('/auth/local', {method: 'POST', body: formData}) .then(() => dispatch(hideSignInDialog())) .catch(error => { if (error.metadata) { - // the user might not have a valid email. prompt the user user re-request the confirmation email - dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata))); + dispatch( + signInFailure(lang.t('error.emailNotVerified', error.metadata)) + ); } else { - // invalid credentials dispatch(signInFailure(lang.t('error.emailPasswordError'))); } @@ -131,9 +144,17 @@ export const fetchSignIn = (formData) => (dispatch) => { // Sign In - Facebook -const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST}); -const signInFacebookSuccess = user => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, user}); -const signInFacebookFailure = error => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, error}); +const signInFacebookRequest = () => ({ + type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST +}); +const signInFacebookSuccess = user => ({ + type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, + user +}); +const signInFacebookFailure = error => ({ + type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, + error +}); export const fetchSignInFacebook = () => dispatch => { dispatch(signInFacebookRequest()); @@ -146,7 +167,9 @@ export const fetchSignInFacebook = () => dispatch => { // Sign Up Facebook -const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST}); +const signUpFacebookRequest = () => ({ + type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST +}); export const fetchSignUpFacebook = () => dispatch => { dispatch(signUpFacebookRequest()); @@ -180,10 +203,14 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = (formData, redirectUri) => (dispatch) => { +export const fetchSignUp = (formData, redirectUri) => dispatch => { dispatch(signUpRequest()); - coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}}) + coralApi('/users', { + method: 'POST', + body: formData, + headers: {'X-Pym-Url': redirectUri} + }) .then(({user}) => { dispatch(signUpSuccess(user)); }) @@ -200,14 +227,23 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => { // Forgot Password Actions -const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST}); -const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS}); -const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE}); +const forgotPassowordRequest = () => ({ + type: actions.FETCH_FORGOT_PASSWORD_REQUEST +}); +const forgotPassowordSuccess = () => ({ + type: actions.FETCH_FORGOT_PASSWORD_SUCCESS +}); +const forgotPassowordFailure = () => ({ + type: actions.FETCH_FORGOT_PASSWORD_FAILURE +}); -export const fetchForgotPassword = email => (dispatch) => { +export const fetchForgotPassword = email => dispatch => { dispatch(forgotPassowordRequest(email)); const redirectUri = pym.parentUrl || location.href; - coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}}) + coralApi('/account/password/reset', { + method: 'POST', + body: {email, loc: redirectUri} + }) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); }; @@ -236,13 +272,17 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error}); // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user, isAdmin) => ({ + type: actions.CHECK_LOGIN_SUCCESS, + user, + isAdmin +}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); coralApi('/auth') - .then((result) => { + .then(result => { if (!result.user) { throw new Error('Not logged in'); } @@ -262,12 +302,15 @@ const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE}); export const requestConfirmEmail = (email, redirectUri) => dispatch => { dispatch(verifyEmailRequest()); - return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}}) + return coralApi('/users/resend-verify', { + method: 'POST', + body: {email}, + headers: {'X-Pym-Url': redirectUri} + }) .then(() => { dispatch(verifyEmailSuccess()); }) .catch(err => { - // email might have already been verifyed dispatch(verifyEmailFailure(err)); }); From 964cc362e79651d8f7f0317c554baa1e43a21f3d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 12 May 2017 18:26:24 -0300 Subject: [PATCH 08/18] Login, logout with tokens. WIP --- client/coral-framework/actions/auth.js | 130 ++++++++++++++------- client/coral-framework/constants/auth.js | 4 +- client/coral-framework/helpers/response.js | 18 ++- client/coral-framework/helpers/storage.js | 65 +++++++++++ client/coral-framework/reducers/auth.js | 2 +- 5 files changed, 164 insertions(+), 55 deletions(-) create mode 100644 client/coral-framework/helpers/storage.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 0f96a463e..a27231cb6 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,11 +1,13 @@ import {gql} from 'react-apollo'; -import client from 'coral-framework/services/client'; -import I18n from '../../coral-framework/modules/i18n/i18n'; -import translations from './../translations'; -const lang = new I18n(translations); +import {pym} from 'coral-framework'; +import * as Storage from '../helpers/storage'; import * as actions from '../constants/auth'; import coralApi, {base} from '../helpers/response'; -import {pym} from 'coral-framework'; +import client from 'coral-framework/services/client'; + +const lang = new I18n(translations); +import translations from './../translations'; +import I18n from '../../coral-framework/modules/i18n/i18n'; const ME_QUERY = gql` query Me { @@ -72,7 +74,10 @@ export const hideCreateUsernameDialog = () => ({ type: actions.HIDE_CREATEUSERNAME_DIALOG }); -const createUsernameSuccess = () => ({type: actions.CREATE_USERNAME_SUCCESS}); +const createUsernameSuccess = () => ({ + type: actions.CREATE_USERNAME_SUCCESS +}); + const createUsernameFailure = error => ({ type: actions.CREATE_USERNAME_FAILURE, error @@ -114,21 +119,41 @@ export const changeView = view => dispatch => { } }; -export const cleanState = () => ({type: actions.CLEAN_STATE}); +export const cleanState = () => ({ + type: actions.CLEAN_STATE +}); // Sign In Actions -const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); +const signInRequest = () => ({ + type: actions.FETCH_SIGNIN_REQUEST +}); -// TODO: revisit login redux flow. -// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); -// -const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); +const signInFailure = error => ({ + type: actions.FETCH_SIGNIN_FAILURE, + error +}); + +//============================================================================== +// AUTH TOKEN +//============================================================================== + +const handleAuthToken = token => dispatch => { + Storage.setItem('token', token); + dispatch({type: 'HANDLE_AUTH_TOKEN'}); +}; + +//============================================================================== +// SIGN IN +//============================================================================== export const fetchSignIn = formData => dispatch => { dispatch(signInRequest()); return coralApi('/auth/local', {method: 'POST', body: formData}) - .then(() => dispatch(hideSignInDialog())) + .then(({token}) => { + dispatch(handleAuthToken(token)); + dispatch(hideSignInDialog()); + }) .catch(error => { if (error.metadata) { // the user might not have a valid email. prompt the user user re-request the confirmation email @@ -142,7 +167,9 @@ export const fetchSignIn = formData => dispatch => { }); }; -// Sign In - Facebook +//============================================================================== +// SIGN IN - FACEBOOK +//============================================================================== const signInFacebookRequest = () => ({ type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST @@ -165,7 +192,9 @@ export const fetchSignInFacebook = () => dispatch => { ); }; -// Sign Up Facebook +//============================================================================== +// SIGN UP - FACEBOOK +//============================================================================== const signUpFacebookRequest = () => ({ type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST @@ -197,7 +226,9 @@ export const facebookCallback = (err, data) => dispatch => { } }; -// Sign Up Actions +//============================================================================== +// SIGN UP +//============================================================================== const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); @@ -225,59 +256,54 @@ export const fetchSignUp = (formData, redirectUri) => dispatch => { }); }; -// Forgot Password Actions +//============================================================================== +// FORGOT PASSWORD +//============================================================================== -const forgotPassowordRequest = () => ({ +const forgotPasswordRequest = () => ({ type: actions.FETCH_FORGOT_PASSWORD_REQUEST }); -const forgotPassowordSuccess = () => ({ + +const forgotPasswordSuccess = () => ({ type: actions.FETCH_FORGOT_PASSWORD_SUCCESS }); -const forgotPassowordFailure = () => ({ + +const forgotPasswordFailure = () => ({ type: actions.FETCH_FORGOT_PASSWORD_FAILURE }); export const fetchForgotPassword = email => dispatch => { - dispatch(forgotPassowordRequest(email)); + dispatch(forgotPasswordRequest(email)); const redirectUri = pym.parentUrl || location.href; coralApi('/account/password/reset', { method: 'POST', body: {email, loc: redirectUri} }) - .then(() => dispatch(forgotPassowordSuccess())) - .catch(error => dispatch(forgotPassowordFailure(error))); + .then(() => dispatch(forgotPasswordSuccess())) + .catch(error => dispatch(forgotPasswordFailure(error))); }; -// LogOut Actions - -const logOutRequest = () => ({type: actions.LOGOUT_REQUEST}); -const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS}); -const logOutFailure = () => ({type: actions.LOGOUT_FAILURE}); +//============================================================================== +// LOGOUT +//============================================================================== export const logout = () => dispatch => { - dispatch(logOutRequest()); - return coralApi('/auth', {method: 'DELETE'}) - .then(() => { - dispatch(logOutSuccess()); - fetchMe(); - }) - .catch(error => dispatch(logOutFailure(error))); + Storage.clear(); + dispatch({type: actions.LOGOUT}); }; -// LogOut Actions - -export const validForm = () => ({type: actions.VALID_FORM}); -export const invalidForm = error => ({type: actions.INVALID_FORM, error}); - -// Check Login +//============================================================================== +// CHECK LOGIN +//============================================================================== const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); +const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); + const checkLoginSuccess = (user, isAdmin) => ({ type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin }); -const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { dispatch(checkLoginRequest()); @@ -295,10 +321,24 @@ export const checkLogin = () => dispatch => { dispatch(checkLoginFailure(`${error.translation_key}`)); }); }; +export const validForm = () => ({type: actions.VALID_FORM}); +export const invalidForm = error => ({type: actions.INVALID_FORM, error}); -const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST}); -const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS}); -const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE}); +//============================================================================== +// VERIFY EMAIL +//============================================================================== + +const verifyEmailRequest = () => ({ + type: actions.VERIFY_EMAIL_REQUEST +}); + +const verifyEmailSuccess = () => ({ + type: actions.VERIFY_EMAIL_SUCCESS +}); + +const verifyEmailFailure = () => ({ + type: actions.VERIFY_EMAIL_FAILURE +}); export const requestConfirmEmail = (email, redirectUri) => dispatch => { dispatch(verifyEmailRequest()); diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index b5d80e32f..24599ac50 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -33,9 +33,7 @@ export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST'; export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS'; export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE'; -export const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; -export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; -export const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; +export const LOGOUT = 'LOGOUT'; export const INVALID_FORM = 'INVALID_FORM'; export const VALID_FORM = 'VALID_FORM'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index 05d515b7d..342577391 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -1,18 +1,22 @@ -export const base = '/api/v1'; +import * as Storage from './storage'; const buildOptions = (inputOptions = {}) => { - const defaultOptions = { method: 'GET', headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' + Accept: 'application/json', + Authorization: `Bearer ${Storage.getItem('token')}`, + 'Content-Type': 'application/json' }, - credentials: 'same-origin', + credentials: 'same-origin' }; let options = Object.assign({}, defaultOptions, inputOptions); - options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers); + options.headers = Object.assign( + {}, + defaultOptions.headers, + inputOptions.headers + ); if (options.method.toLowerCase() !== 'get') { options.body = JSON.stringify(options.body); @@ -45,6 +49,8 @@ const handleResp = res => { } }; +export const base = '/api/v1'; + export default (url, options) => { return fetch(`${base}${url}`, buildOptions(options)).then(handleResp); }; diff --git a/client/coral-framework/helpers/storage.js b/client/coral-framework/helpers/storage.js new file mode 100644 index 000000000..aea85bdeb --- /dev/null +++ b/client/coral-framework/helpers/storage.js @@ -0,0 +1,65 @@ +let available, error; + +function storageAvailable(type) { + try { + let storage = window[type], x = '__storage_test__'; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } catch (e) { + error = e; + return ( + e instanceof DOMException && + + // everything except Firefox + (e.code === 22 || + + // Firefox + + e.code === 1014 || + + // test name field too, because code might not be present + + // everything except Firefox + e.name === 'QuotaExceededError' || + + // Firefox + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + + // acknowledge QuotaExceededError only if there's something already stored + storage.length !== 0 + ); + } +} + +export function getItem(item = '') { + if (typeof available === 'undefined') { + available = storageAvailable('localStorage'); + } + + if (available) { + return localStorage.getItem(item); + } else { + console.error(`Cannot get from localStorage. localStorage is not available. ${error}`); + } +} + +export function setItem(item = '', value) { + if (typeof available === 'undefined') { + available = storageAvailable('localStorage'); + } + + if (available) { + return localStorage.setItem(item, value); + } else { + console.error(`Cannot set localStorage. localStorage is not available. ${error}`); + } +} + +export function clear() { + if (available) { + return localStorage.clear(); + } else { + console.error(`Cannot clear localStorage. localStorage is not available. ${error}`); + } +} diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index dc68a442c..34a8dbcd5 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -113,7 +113,7 @@ export default function auth (state = initialState, action) { return state .set('isLoading', false) .set('successSignUp', true); - case actions.LOGOUT_SUCCESS: + case actions.LOGOUT: return state .set('user', null) .set('isLoading', false) From e218de811b1c7913cc91fd4bd14970092ba77174 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 12 May 2017 15:53:25 -0600 Subject: [PATCH 09/18] removed csrf reference --- views/admin/confirm-email.ejs | 3 --- views/admin/password-reset.ejs | 4 ---- 2 files changed, 7 deletions(-) diff --git a/views/admin/confirm-email.ejs b/views/admin/confirm-email.ejs index 6b4d456d8..20d213fa2 100644 --- a/views/admin/confirm-email.ejs +++ b/views/admin/confirm-email.ejs @@ -74,9 +74,6 @@ url: '/api/v1/account/email/verify', contentType: 'application/json', method: 'POST', - headers: { - 'X-CSRF-Token': '<%= csrfToken %>' - }, data: JSON.stringify({token: location.hash.replace('#', '')}) }).then(function (success) { location.href = success.redirectUri; diff --git a/views/admin/password-reset.ejs b/views/admin/password-reset.ejs index 4b25d1279..704b5213d 100644 --- a/views/admin/password-reset.ejs +++ b/views/admin/password-reset.ejs @@ -82,7 +82,6 @@
- Set new password