Merge pull request #17238 from raisedadead/feat/authentication

fix(auth): Add verification route for email
This commit is contained in:
Stuart Taylor
2018-05-28 22:34:56 +01:00
committed by GitHub
12 changed files with 231 additions and 107 deletions

View File

@ -132,9 +132,9 @@ class EmailSettings extends PureComponent {
<FullWidthRow> <FullWidthRow>
<HelpBlock> <HelpBlock>
<Alert bsStyle='info'> <Alert bsStyle='info'>
A change of email address has not been verified. Your email has not been verified.
To use your new email, you must verify it first using the link To use your email, you must
we sent you. <a href='/update-email'> verify it here first</a>.
</Alert> </Alert>
</HelpBlock> </HelpBlock>
</FullWidthRow> </FullWidthRow>

View File

@ -407,10 +407,15 @@ module.exports = function(User) {
); );
}; };
User.afterRemote('logout', function(ctx, result, next) { User.afterRemote('logout', function({req, res}, result, next) {
var res = ctx.res; const config = {
res.clearCookie('access_token'); signed: !!req.signedCookies,
res.clearCookie('userId'); domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
next(); next();
}); });
@ -571,14 +576,9 @@ module.exports = function(User) {
this.update$({ emailAuthLinkTTL }) this.update$({ emailAuthLinkTTL })
); );
}) })
.map(() => isSignUp ? .map(() =>
dedent` dedent`
We created a new account for you! Check your email and click the link we sent you to confirm you email.
Check your email and click the sign in link we sent you.
` :
dedent`
We found your existing account.
Check your email and click the sign in link we sent you.
` `
); );
} }
@ -586,60 +586,60 @@ module.exports = function(User) {
User.prototype.requestAuthEmail = requestAuthEmail; User.prototype.requestAuthEmail = requestAuthEmail;
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) { User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
const currentEmail = this.email; const currentEmail = this.email;
return Observable.defer(() => { const isOwnEmail = isTheSame(newEmail, currentEmail);
const isOwnEmail = isTheSame(newEmail, currentEmail); const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail);
const sameUpdate = isTheSame(newEmail, this.newEmail); const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL);
const messageOrNull = getWaitMessage(this.emailVerifyTTL); const isVerifiedEmail = this.emailVerified;
if (isOwnEmail) {
if (this.emailVerified) { if (isOwnEmail && isVerifiedEmail) {
// email is already associated and verified with this account // email is already associated and verified with this account
throw wrapHandledError( throw wrapHandledError(
new Error('email is already verified'), new Error('email is already verified'),
{ {
type: 'info', type: 'info',
message: `${newEmail} is already associated with this account.` message: `
} ${newEmail} is already associated with this account.
); You can update a new email address instead.`
} else if (!this.emailVerified && messageOrNull) { }
// email is associated but unverified and );
// email is within time limit }
throw wrapHandledError( if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
new Error(), // trying to update with the same newEmail and
{ // confirmation email is still valid
type: 'info', throw wrapHandledError(
message: messageOrNull new Error(),
} {
); type: 'info',
} message: dedent`
} We have already sent an email confirmation request to ${newEmail}.
if (sameUpdate && messageOrNull) { ${isLinkSentWithinLimit}`
// trying to update with the same newEmail and }
// confirmation email is still valid );
throw wrapHandledError( }
new Error(), if (!isEmail('' + newEmail)) {
{ throw createEmailError();
type: 'info', }
message: dedent`
We have already sent an email confirmation request to ${newEmail}. // newEmail is not associated with this user, and
Please check your inbox.` // this attempt to change email is the first or
} // previous attempts have expired
); if (
} !isOwnEmail ||
if (!isEmail('' + newEmail)) { (isOwnEmail && !isVerifiedEmail) ||
throw createEmailError(); (isResendUpdateToSameEmail && !isLinkSentWithinLimit)
} ) {
// newEmail is not associated with this user, and const updateConfig = {
// this attempt to change email is the first or newEmail,
// previous attempts have expired emailVerified: false,
return Observable.if( emailVerifyTTL: new Date()
() => isOwnEmail || (sameUpdate && messageOrNull), };
Observable.empty(),
// defer prevents the promise from firing prematurely (before subscribe) // defer prevents the promise from firing prematurely (before subscribe)
Observable.defer(() => User.doesExist(null, newEmail)) return Observable.defer(() => User.doesExist(null, newEmail))
)
.do(exists => { .do(exists => {
if (exists) { if (exists && !isOwnEmail) {
// newEmail is not associated with this account, // newEmail is not associated with this account,
// but is associated with different account // but is associated with different account
throw wrapHandledError( throw wrapHandledError(
@ -652,17 +652,20 @@ module.exports = function(User) {
); );
} }
}) })
.flatMap(() => { .flatMap(()=>{
const update = { return Observable.forkJoin(
newEmail, this.update$(updateConfig),
emailVerified: false, this.requestAuthEmail(false, newEmail),
emailVerifyTTL: new Date() (_, message) => message
}; )
return this.update$(update) .do(() => {
.do(() => Object.assign(this, update)) Object.assign(this, updateConfig);
.flatMap(() => this.requestAuthEmail(false, newEmail)); });
}); });
});
} else {
return 'Something unexpected happened whilst updating your email.';
}
}; };
function requestCompletedChallenges() { function requestCompletedChallenges() {

View File

@ -38,9 +38,37 @@ module.exports = function enableAuthentication(app) {
ifUserRedirect, ifUserRedirect,
(req, res) => res.redirect(301, '/auth/auth0')); (req, res) => res.redirect(301, '/auth/auth0'));
router.get(
'/update-email',
ifNoUserRedirectHome,
(req, res) => res.render('account/update-email', {
title: 'Update your email'
})
);
router.get('/signout', (req, res) => { router.get('/signout', (req, res) => {
req.logout(); req.logout();
res.redirect('/'); req.session.destroy( (err) => {
if (err) {
throw wrapHandledError(
new Error('could not destroy session'),
{
type: 'info',
message: 'Oops, something is not right.',
redirectTo: '/'
}
);
}
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
res.redirect('/');
});
}); });
router.get( router.get(
@ -165,21 +193,11 @@ module.exports = function enableAuthentication(app) {
// update user and log them in // update user and log them in
.map(user => user.loginByRequest(req, res)) .map(user => user.loginByRequest(req, res))
.do(() => { .do(() => {
let redirectTo = '/';
if (
req.session &&
req.session.returnTo
) {
redirectTo = req.session.returnTo;
}
req.flash( req.flash(
'success', 'success',
'Success! You have signed in to your account. Happy Coding!' 'Success! You have signed in to your account. Happy Coding!'
); );
return res.redirect('/');
return res.redirect(redirectTo);
}) })
.subscribe( .subscribe(
() => {}, () => {},

View File

@ -5,12 +5,12 @@ import { curry } from 'lodash';
import { import {
ifNoUser401, ifNoUser401,
ifNoUserRedirectTo, ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings ifNotVerifiedRedirectToUpdateEmail
} from '../utils/middleware'; } from '../utils/middleware';
const debug = debugFactory('fcc:boot:user'); const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map'); const sendNonUserToHome = ifNoUserRedirectTo('/');
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map'); const sendNonUserToHomeWithMessage = curry(ifNoUserRedirectTo, 2)('/');
module.exports = function(app) { module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
@ -24,7 +24,7 @@ module.exports = function(app) {
); );
api.get( api.get(
'/account', '/account',
sendNonUserToMap, sendNonUserToHome,
getAccount getAccount
); );
api.post( api.post(
@ -34,15 +34,15 @@ module.exports = function(app) {
); );
api.get( api.get(
'/account/unlink/:social', '/account/unlink/:social',
sendNonUserToMap, sendNonUserToHome,
getUnlinkSocial getUnlinkSocial
); );
// Ensure these are the last routes! // Ensure these are the last routes!
router.get( router.get(
'/user/:username/report-user/', '/user/:username/report-user/',
sendNonUserToMapWithMessage('You must be signed in to report a user'), sendNonUserToHomeWithMessage('You must be signed in to report a user'),
ifNotVerifiedRedirectToSettings, ifNotVerifiedRedirectToUpdateEmail,
getReportUserProfile getReportUserProfile
); );
@ -119,6 +119,14 @@ module.exports = function(app) {
if (err) { return next(err); } if (err) { return next(err); }
req.logout(); req.logout();
req.flash('success', 'You have successfully deleted your account.'); req.flash('success', 'You have successfully deleted your account.');
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
return res.status(200).end(); return res.status(200).end();
}); });
} }

View File

@ -106,9 +106,8 @@ export default function setupPassport(app) {
// https://stackoverflow.com/q/37430452 // https://stackoverflow.com/q/37430452
let successRedirect = (req) => { let successRedirect = (req) => {
if (!!req && req.session && req.session.returnTo) { if (!!req && req.session && req.session.returnTo) {
let returnTo = req.session.returnTo;
delete req.session.returnTo; delete req.session.returnTo;
return returnTo; return '/';
} }
return config.successRedirect || ''; return config.successRedirect || '';
}; };

View File

@ -58,6 +58,7 @@
"./middlewares/jade-helpers": {}, "./middlewares/jade-helpers": {},
"./middlewares/flash-cheaters": {}, "./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {}, "./middlewares/passport-login": {},
"./middlewares/email-not-verified-notice": {},
"./middlewares/privacy-terms-notice": {} "./middlewares/privacy-terms-notice": {}
}, },
"files": {}, "files": {},

View File

@ -0,0 +1,35 @@
import dedent from 'dedent';
const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/signout',
'/accept-privacy-terms',
'/update-email',
'/passwordless-change',
'/external/services/user'
];
export default function emailNotVerifiedNotice() {
return function(req, res, next) {
if (
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
EXCLUDED_PATHS.indexOf(req.path) === -1
) {
const { user } = req;
if (user && (!user.email || user.email === '' || !user.emailVerified)) {
req.flash(
'danger',
dedent`
New privacy laws now require that we have an email address where we can reach
you. Please verify your email address below and click the link we send you to
confirm.
`
);
res.redirect('/update-email');
return next;
}
}
return next();
};
}

View File

@ -2,7 +2,10 @@ const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [ const EXCLUDED_PATHS = [
'/api/flyers/findOne', '/api/flyers/findOne',
'/signout', '/signout',
'/accept-privacy-terms' '/accept-privacy-terms',
'/update-email',
'/passwordless-change',
'/external/services/user'
]; ];
export default function privacyTermsNotAcceptedNotice() { export default function privacyTermsNotAcceptedNotice() {

View File

@ -32,7 +32,7 @@ export function ifNoUser401(req, res, next) {
return res.status(401).end(); return res.status(401).end();
} }
export function ifNotVerifiedRedirectToSettings(req, res, next) { export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
const { user } = req; const { user } = req;
if (!user) { if (!user) {
return next(); return next();

View File

@ -1,12 +1,12 @@
extends ../layout extends ../layout
block content block content
.container .row.flashMessage.negative-30
.row.flashMessage.negative-30 .col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
.col-sm-6.col-sm-offset-3 #flash-board.alert.fade.in(style='display: none;')
#flash-board.alert.fade.in(style='display: none;') button.close(type='button', data-dismiss='alert')
button.close(type='button', data-dismiss='alert') span.ion-close-circled#flash-close
span.ion-close-circled#flash-close #flash-content
#flash-content .row
.col-xs-12 .col-xs-12
#accept-privacy-terms #accept-privacy-terms
.row .row

View File

@ -0,0 +1,57 @@
extends ../layout
block content
.row.flashMessage.negative-30
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
#flash-board.alert.fade.in(style='display: none;')
button.close(type='button', data-dismiss='alert')
span.ion-close-circled#flash-close
#flash-content
.container
h2.text-center Update your email address here:
form.form-horizontal.update-email(method='POST', action='/update-my-email', name="updateEmailForm")
.row
.col-sm-6.col-sm-offset-3
input(type='hidden', name='_csrf', value=_csrf)
.form-group
input.input-lg.form-control(type='email', name='email', id='email', value=user.email || '', placeholder=user.email || 'Enter your new email', autofocus, required, autocomplete="off")
.form-group
button.btn.btn-lg.btn-primary.btn-block(type='submit')= !user.email || user.emailVerified ? 'Update my Email' : 'Verify Email'
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/signout')
| Sign out
script.
$(document).ready(function() {
$('form').submit(function(event){
event.preventDefault();
$('#flash-board').hide();
var $form = $(event.target);
$.ajax({
type : 'POST',
url : $form.attr('action'),
data : $form.serialize(),
dataType : 'json',
encode : true,
xhrFields : { withCredentials: true }
})
.fail(error => {
if (error.responseText){
var data = JSON.parse(error.responseText);
if(data.message)
$('#flash-content').html(data.message);
$('#flash-board')
.removeClass('alert-success')
.addClass('alert-info')
.fadeIn();
}
})
.done(data =>{
if(data && data.message){
$('#flash-content').html(data.message);
$('#flash-board')
.removeClass('alert-info')
.addClass('alert-success')
.fadeIn();
}
});
});
});

View File

@ -14,7 +14,7 @@ nav.navbar.navbar-default.navbar-static-top.nav-height
a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum
if !user if !user
li li
a(href='/signin') Start Coding a(href='/signin') Sign in
else else
li li
a(href='/settings') Settings a(href='/settings') Settings