Skip to content

Commit

Permalink
fix(auth): Fix auth flow for the client app
Browse files Browse the repository at this point in the history
  • Loading branch information
Bouncey authored and raisedadead committed Oct 24, 2018
1 parent a656cbf commit c08bb95
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 213 deletions.
18 changes: 9 additions & 9 deletions api-server/server/boot/authentication.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import _ from 'lodash';
import { Observable } from 'rx';
import dedent from 'dedent';
// import debugFactory from 'debug';
import passport from 'passport';
import { isEmail } from 'validator';
import { check } from 'express-validator/check';

import { homeLocation } from '../../../config/env';

import { createCookieConfig } from '../utils/cookieConfig';
import { createPassportCallbackAuthenticator } from '../component-passport';
import {
ifUserRedirectTo,
ifNoUserRedirectTo,
Expand All @@ -15,7 +16,6 @@ import {
import { wrapHandledError } from '../utils/create-handled-error.js';

const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
// const debug = debugFactory('fcc:boot:auth');
if (isSignUpDisabled) {
console.log('fcc:boot:auth - Sign up is disabled');
}
Expand All @@ -29,7 +29,11 @@ module.exports = function enableAuthentication(app) {
const api = app.loopback.Router();
const { AuthToken, User } = app.models;

api.get('/signin', ifUserRedirect, (req, res) => res.redirect('/auth/auth0'));
api.get('/signin', ifUserRedirect, passport.authenticate('auth0-login', {}));
api.get(
'/auth/auth0/callback',
createPassportCallbackAuthenticator('auth0-login', { provider: 'auth0' })
);

api.get('/signout', (req, res) => {
req.logout();
Expand All @@ -41,10 +45,7 @@ module.exports = function enableAuthentication(app) {
redirectTo: homeLocation
});
}
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
const config = createCookieConfig(req);
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
Expand Down Expand Up @@ -216,5 +217,4 @@ module.exports = function enableAuthentication(app) {
);

app.use(api);
app.use('/internal', api);
};
7 changes: 4 additions & 3 deletions api-server/server/boot/z-not-found.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import accepts from 'accepts';

import { homeLocation } from '../../../config/env.json';

export default function fourOhFour(app) {
app.all('*', function(req, res) {
const accept = accepts(req);
const type = accept.type('html', 'json', 'text');
const { path } = req;


if (type === 'html') {
req.flash('danger', `We couldn't find path ${ path }`);
return res.render('404', { title: '404'});
req.flash('danger', `We couldn't find path ${path}`);
return res.redirectWithFlash(`${homeLocation}/404`);
}

if (type === 'json') {
Expand Down
181 changes: 98 additions & 83 deletions api-server/server/component-passport.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import passport from 'passport';
import { PassportConfigurator } from
'@freecodecamp/loopback-component-passport';
import passportProviders from './passport-providers';
import {
PassportConfigurator
} from '@freecodecamp/loopback-component-passport';
import url from 'url';
import jwt from 'jsonwebtoken';
import dedent from 'dedent';

import { homeLocation } from '../../config/env.json';
import { jwtSecret } from '../../config/secrets';
import passportProviders from './passport-providers';
import { createCookieConfig } from './utils/cookieConfig';

const passportOptions = {
emailOptional: true,
profileToUser: null
Expand All @@ -23,15 +28,14 @@ function getCompletedCertCount(user) {
'isInfosecQaCert',
'isJsAlgoDataStructCert',
'isRespWebDesignCert'
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
].reduce((sum, key) => (user[key] ? sum + 1 : sum), 0);
}

function getLegacyCertCount(user) {
return [
'isFrontEndCert',
'isBackEndCert',
'isDataVisCert'
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
return ['isFrontEndCert', 'isBackEndCert', 'isDataVisCert'].reduce(
(sum, key) => (user[key] ? sum + 1 : sum),
0
);
}

PassportConfigurator.prototype.init = function passportInit(noSession) {
Expand All @@ -51,7 +55,6 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
});

passport.deserializeUser((id, done) => {

this.userModel.findById(id, { fields }, (err, user) => {
if (err || !user) {
return done(err, user);
Expand All @@ -62,8 +65,12 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
.aggregate([
{ $match: { _id: user.id } },
{ $project: { points: { $size: '$progressTimestamps' } } }
]).get(function(err, [{ points = 1 } = {}]) {
if (err) { console.error(err); return done(err); }
])
.get(function(err, [{ points = 1 } = {}]) {
if (err) {
console.error(err);
return done(err);
}
user.points = points;
let completedChallengeCount = 0;
let completedProjectCount = 0;
Expand All @@ -90,6 +97,15 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
};

export function setupPassport(app) {
// NOTE(Bouncey): Not sure this is doing much now
// Loopback complains about userCredentialModle when this
// setup is remoed from server/server.js
//
// I have split the custom callback in to it's own export that we can use both
// here and in boot:auth
//
// Needs more investigation...

const configurator = new PassportConfigurator(app);

configurator.setupModels({
Expand All @@ -104,78 +120,77 @@ export function setupPassport(app) {
let config = passportProviders[strategy];
config.session = config.session !== false;

// https://stackoverflow.com/q/37430452
let successRedirect = (req) => {
if (!!req && req.session && req.session.returnTo) {
delete req.session.returnTo;
return '/';
}
return config.successRedirect || '';
};

config.customCallback = !config.useCustomCallback
? null
: (req, res, next) => {

passport.authenticate(
strategy,
{ session: false },
(err, user, userInfo) => {

if (err) {
return next(err);
}

if (!user || !userInfo) {
return res.redirect(config.failureRedirect);
}
let redirect = url.parse(successRedirect(req), true);

delete redirect.search;

const { accessToken } = userInfo;
const { provider } = config;
if (accessToken && accessToken.id) {
if (provider === 'auth0') {
req.flash(
'success',
dedent`
Success! You have signed in to your account. Happy Coding!
`
);
} else if (user.email) {
req.flash(
'info',
dedent`
We are moving away from social authentication for privacy reasons. Next time
we recommend using your email address: ${user.email} to sign in instead.
`
);
}
const cookieConfig = {
signed: !!req.signedCookies,
maxAge: accessToken.ttl,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET);
res.cookie('jwt_access_token', jwtAccess, cookieConfig);
res.cookie('access_token', accessToken.id, cookieConfig);
res.cookie('userId', accessToken.userId, cookieConfig);
req.login(user);
}

redirect = url.format(redirect);
return res.redirect(redirect);
}
)(req, res, next);
};

configurator.configureProvider(
strategy,
{
...config,
...passportOptions
}
);
: createPassportCallbackAuthenticator(strategy, config);

configurator.configureProvider(strategy, {
...config,
...passportOptions
});
});
}

export const createPassportCallbackAuthenticator = (strategy, config) => (
req,
res,
next
) => {
// https://stackoverflow.com/q/37430452
const successRedirect = req => {
if (!!req && req.session && req.session.returnTo) {
delete req.session.returnTo;
return `${homeLocation}/welcome`;
}
return config.successRedirect || `${homeLocation}/welcome`;
};
return passport.authenticate(
strategy,
{ session: false },
(err, user, userInfo) => {
if (err) {
return next(err);
}

if (!user || !userInfo) {
return res.redirect('/signin');
}
let redirect = url.parse(successRedirect(req), true);

delete redirect.search;

const { accessToken } = userInfo;
const { provider } = config;
if (accessToken && accessToken.id) {
if (provider === 'auth0') {
req.flash(
'success',
dedent`
Success! You have signed in to your account. Happy Coding!
`
);
} else if (user.email) {
req.flash(
'info',
dedent`
We are moving away from social authentication for privacy reasons. Next time
we recommend using your email address: ${user.email} to sign in instead.
`
);
}
const cookieConfig = {
...createCookieConfig(req),
maxAge: accessToken.ttl
};
const jwtAccess = jwt.sign({ accessToken }, jwtSecret);
res.cookie('jwt_access_token', jwtAccess, cookieConfig);
res.cookie('access_token', accessToken.id, cookieConfig);
res.cookie('userId', accessToken.userId, cookieConfig);
req.login(user);
}

redirect = url.format(redirect);
return res.redirect(redirect);
}
)(req, res, next);
};
2 changes: 1 addition & 1 deletion api-server/server/middlewares/passport-login.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { login } from 'passport/lib/http/request';
// if called without callback it returns an observable
// login(user, options?, cb?) => Void|Observable
function login$(...args) {
console.log('args');
if (_.isFunction(_.last(args))) {
return login.apply(this, args);
}
return Observable.fromNodeCallback(login).apply(this, args);
}

export default function passportLogin() {
return (req, res, next) => {
req.login = req.logIn = login$;
Expand Down
15 changes: 10 additions & 5 deletions api-server/server/passport-providers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const successRedirect = '/';
const failureRedirect = '/';
import { auth0 } from '../../config/secrets';
import { homeLocation } from '../../config/env.json';

const { clientID, clientSecret, domain } = auth0;

const successRedirect = `${homeLocation}/welcome`;
const failureRedirect = '/signin';

export default {
local: {
Expand All @@ -16,9 +21,9 @@ export default {
'auth0-login': {
provider: 'auth0',
module: 'passport-auth0',
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
domain: process.env.AUTH0_DOMAIN,
clientID,
clientSecret,
domain,
cookieDomain: 'freeCodeCamp.org',
callbackURL: '/auth/auth0/callback',
authPath: '/auth/auth0',
Expand Down
28 changes: 20 additions & 8 deletions api-server/server/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env')});
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });

const _ = require('lodash');
const Rx = require('rx');
Expand All @@ -17,7 +17,6 @@ log.enabled = true;

Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production';
const app = loopback();
const isBeta = !!process.env.BETA;

expressState.extend(app);
app.set('state namespace', '__fcc__');
Expand All @@ -27,9 +26,25 @@ app.set('view engine', 'jade');
app.use(loopback.token());
app.disable('x-powered-by');

boot(app, {
appRootDir: __dirname,
dev: process.env.NODE_ENV
const createLogOnce = () => {
let called = false;
return str => {
if (called) {
return null;
}
called = true;
return log(str);
};
};
const logOnce = createLogOnce();

boot(app, __dirname, err => {
if (err) {
// rethrowing the error here bacause any error thrown in the boot stage
// is silent
logOnce('The below error was thrown in the boot stage');
throw err;
}
});

setupPassport(app);
Expand All @@ -44,9 +59,6 @@ app.start = _.once(function() {
app.get('port'),
app.get('env')
);
if (isBeta) {
log('freeCodeCamp is in beta mode');
}
log(`connecting to db at ${db.settings.url}`);
});

Expand Down
Loading

0 comments on commit c08bb95

Please sign in to comment.