2017-12-27 18:11:17 +00:00
|
|
|
import _ from 'lodash';
|
|
|
|
import { Observable } from 'rx';
|
2017-12-26 21:20:03 +00:00
|
|
|
import dedent from 'dedent';
|
2018-10-23 23:24:48 +00:00
|
|
|
import passport from 'passport';
|
2017-12-27 18:11:17 +00:00
|
|
|
import { isEmail } from 'validator';
|
2018-01-23 01:08:33 +00:00
|
|
|
import { check } from 'express-validator/check';
|
2017-12-27 18:11:17 +00:00
|
|
|
|
2018-08-31 15:04:04 +00:00
|
|
|
import { homeLocation } from '../../../config/env';
|
2018-10-23 23:24:48 +00:00
|
|
|
import { createCookieConfig } from '../utils/cookieConfig';
|
|
|
|
import { createPassportCallbackAuthenticator } from '../component-passport';
|
2017-12-27 18:11:17 +00:00
|
|
|
import {
|
2018-01-23 01:08:33 +00:00
|
|
|
ifUserRedirectTo,
|
2018-02-16 23:18:53 +00:00
|
|
|
ifNoUserRedirectTo,
|
2018-01-23 01:08:33 +00:00
|
|
|
createValidatorErrorHandler
|
|
|
|
} from '../utils/middleware';
|
|
|
|
import { wrapHandledError } from '../utils/create-handled-error.js';
|
2017-12-26 21:20:03 +00:00
|
|
|
|
|
|
|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
2018-01-01 23:01:50 +00:00
|
|
|
if (isSignUpDisabled) {
|
|
|
|
console.log('fcc:boot:auth - Sign up is disabled');
|
|
|
|
}
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2015-06-03 19:26:11 +00:00
|
|
|
module.exports = function enableAuthentication(app) {
|
2017-12-26 21:20:03 +00:00
|
|
|
// enable loopback access control authentication. see:
|
2018-06-28 09:32:22 +00:00
|
|
|
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
|
2015-06-03 19:26:11 +00:00
|
|
|
app.enableAuth();
|
2017-12-27 18:11:17 +00:00
|
|
|
const ifUserRedirect = ifUserRedirectTo();
|
2018-08-29 19:52:41 +00:00
|
|
|
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
|
2017-12-26 21:20:03 +00:00
|
|
|
const api = app.loopback.Router();
|
2017-12-29 17:59:27 +00:00
|
|
|
const { AuthToken, User } = app.models;
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2018-10-23 23:24:48 +00:00
|
|
|
api.get('/signin', ifUserRedirect, passport.authenticate('auth0-login', {}));
|
|
|
|
api.get(
|
|
|
|
'/auth/auth0/callback',
|
|
|
|
createPassportCallbackAuthenticator('auth0-login', { provider: 'auth0' })
|
|
|
|
);
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2018-08-29 19:52:41 +00:00
|
|
|
api.get('/signout', (req, res) => {
|
2017-12-26 21:20:03 +00:00
|
|
|
req.logout();
|
2018-08-29 19:52:41 +00:00
|
|
|
req.session.destroy(err => {
|
2018-05-25 17:44:09 +00:00
|
|
|
if (err) {
|
2018-08-29 19:52:41 +00:00
|
|
|
throw wrapHandledError(new Error('could not destroy session'), {
|
|
|
|
type: 'info',
|
|
|
|
message: 'Oops, something is not right.',
|
|
|
|
redirectTo: homeLocation
|
|
|
|
});
|
2018-05-25 17:44:09 +00:00
|
|
|
}
|
2018-10-23 23:24:48 +00:00
|
|
|
const config = createCookieConfig(req);
|
2018-05-26 12:58:20 +00:00
|
|
|
res.clearCookie('jwt_access_token', config);
|
|
|
|
res.clearCookie('access_token', config);
|
|
|
|
res.clearCookie('userId', config);
|
|
|
|
res.clearCookie('_csrf', config);
|
2018-08-29 19:52:41 +00:00
|
|
|
res.redirect(homeLocation);
|
|
|
|
});
|
2017-12-27 18:11:17 +00:00
|
|
|
});
|
2017-12-26 21:20:03 +00:00
|
|
|
|
|
|
|
const defaultErrorMsg = dedent`
|
|
|
|
Oops, something is not right,
|
|
|
|
please request a fresh link to sign in / sign up.
|
|
|
|
`;
|
|
|
|
|
2017-12-27 18:11:17 +00:00
|
|
|
const passwordlessGetValidators = [
|
|
|
|
check('email')
|
|
|
|
.isBase64()
|
2018-01-01 23:39:14 +00:00
|
|
|
.withMessage('Email should be a base64 encoded string.'),
|
2017-12-27 18:11:17 +00:00
|
|
|
check('token')
|
|
|
|
.exists()
|
2018-01-01 23:39:14 +00:00
|
|
|
.withMessage('Token should exist.')
|
2017-12-27 18:11:17 +00:00
|
|
|
// based on strongloop/loopback/common/models/access-token.js#L15
|
|
|
|
.isLength({ min: 64, max: 64 })
|
2018-01-01 23:39:14 +00:00
|
|
|
.withMessage('Token is not the right length.')
|
2017-12-27 18:11:17 +00:00
|
|
|
];
|
|
|
|
|
2017-12-26 21:20:03 +00:00
|
|
|
function getPasswordlessAuth(req, res, next) {
|
2017-12-27 18:11:17 +00:00
|
|
|
const {
|
2018-08-29 19:52:41 +00:00
|
|
|
query: { email: encodedEmail, token: authTokenId, emailChange } = {}
|
2017-12-27 18:11:17 +00:00
|
|
|
} = req;
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2017-12-27 18:11:17 +00:00
|
|
|
const email = User.decodeEmail(encodedEmail);
|
|
|
|
if (!isEmail(email)) {
|
2018-08-29 19:52:41 +00:00
|
|
|
return next(
|
|
|
|
wrapHandledError(new TypeError('decoded email is invalid'), {
|
2017-12-27 18:11:17 +00:00
|
|
|
type: 'info',
|
|
|
|
message: 'The email encoded in the link is incorrectly formatted',
|
2018-08-29 19:52:41 +00:00
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
})
|
|
|
|
);
|
2017-12-26 21:20:03 +00:00
|
|
|
}
|
2017-12-27 18:11:17 +00:00
|
|
|
// first find
|
2018-08-29 19:52:41 +00:00
|
|
|
return (
|
|
|
|
AuthToken.findOne$({ where: { id: authTokenId } })
|
|
|
|
.flatMap(authToken => {
|
|
|
|
if (!authToken) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error(`no token found for id: ${authTokenId}`),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// find user then validate and destroy email validation token
|
|
|
|
// finally retun user instance
|
|
|
|
return User.findOne$({ where: { id: authToken.userId } }).flatMap(
|
|
|
|
user => {
|
|
|
|
if (!user) {
|
2018-02-16 23:18:53 +00:00
|
|
|
throw wrapHandledError(
|
2018-08-29 19:52:41 +00:00
|
|
|
new Error(`no user found for token: ${authTokenId}`),
|
2018-02-16 23:18:53 +00:00
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
2018-08-29 19:52:41 +00:00
|
|
|
redirectTo: `${homeLocation}/signin`
|
2018-02-16 23:18:53 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2018-08-29 19:52:41 +00:00
|
|
|
if (user.email !== email) {
|
|
|
|
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
2017-12-27 18:11:17 +00:00
|
|
|
throw wrapHandledError(
|
2018-08-29 19:52:41 +00:00
|
|
|
new Error('user email does not match'),
|
2017-12-27 18:11:17 +00:00
|
|
|
{
|
2018-08-29 19:52:41 +00:00
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return authToken
|
|
|
|
.validate$()
|
|
|
|
.map(isValid => {
|
|
|
|
if (!isValid) {
|
|
|
|
throw wrapHandledError(new Error('token is invalid'), {
|
2017-12-27 18:11:17 +00:00
|
|
|
type: 'info',
|
|
|
|
message: `
|
|
|
|
Looks like the link you clicked has expired,
|
|
|
|
please request a fresh link, to sign in.
|
|
|
|
`,
|
2018-08-29 19:52:41 +00:00
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return authToken.destroy$();
|
|
|
|
})
|
|
|
|
.map(() => user);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
})
|
|
|
|
// at this point token has been validated and destroyed
|
|
|
|
// update user and log them in
|
|
|
|
.map(user => user.loginByRequest(req, res))
|
|
|
|
.do(() => {
|
|
|
|
req.flash(
|
|
|
|
'success',
|
|
|
|
'Success! You have signed in to your account. Happy Coding!'
|
|
|
|
);
|
2018-08-30 14:36:26 +00:00
|
|
|
return res.redirectWithFlash(`${homeLocation}/welcome`);
|
2018-08-29 19:52:41 +00:00
|
|
|
})
|
|
|
|
.subscribe(() => {}, next)
|
|
|
|
);
|
2017-12-27 18:11:17 +00:00
|
|
|
}
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2018-08-29 19:52:41 +00:00
|
|
|
api.get(
|
2017-12-27 18:11:17 +00:00
|
|
|
'/passwordless-auth',
|
|
|
|
ifUserRedirect,
|
|
|
|
passwordlessGetValidators,
|
2018-08-29 19:52:41 +00:00
|
|
|
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
2017-12-27 18:11:17 +00:00
|
|
|
getPasswordlessAuth
|
|
|
|
);
|
|
|
|
|
2018-08-29 19:52:41 +00:00
|
|
|
api.get('/passwordless-change', (req, res) =>
|
|
|
|
res.redirect(301, '/confirm-email')
|
2018-07-28 07:04:27 +00:00
|
|
|
);
|
2018-08-29 19:52:41 +00:00
|
|
|
|
|
|
|
api.get(
|
2018-07-28 07:04:27 +00:00
|
|
|
'/confirm-email',
|
2018-02-16 23:18:53 +00:00
|
|
|
ifNoUserRedirectHome,
|
|
|
|
passwordlessGetValidators,
|
|
|
|
getPasswordlessAuth
|
|
|
|
);
|
|
|
|
|
2017-12-27 18:11:17 +00:00
|
|
|
const passwordlessPostValidators = [
|
|
|
|
check('email')
|
|
|
|
.isEmail()
|
2018-01-01 23:39:14 +00:00
|
|
|
.withMessage('Email is not a valid email address.')
|
2017-12-27 18:11:17 +00:00
|
|
|
];
|
|
|
|
function postPasswordlessAuth(req, res, next) {
|
|
|
|
const { body: { email } = {} } = req;
|
2017-12-26 21:20:03 +00:00
|
|
|
|
2017-12-27 18:11:17 +00:00
|
|
|
return User.findOne$({ where: { email } })
|
2018-08-29 19:52:41 +00:00
|
|
|
.flatMap(_user =>
|
|
|
|
Observable.if(
|
2017-12-29 04:38:16 +00:00
|
|
|
// if no user found create new user and save to db
|
|
|
|
_.constant(_user),
|
|
|
|
Observable.of(_user),
|
|
|
|
User.create$({ email })
|
2018-08-29 19:52:41 +00:00
|
|
|
).flatMap(user => user.requestAuthEmail(!_user))
|
2017-12-29 04:38:16 +00:00
|
|
|
)
|
2018-04-13 14:48:10 +00:00
|
|
|
.do(msg => {
|
2018-08-29 19:52:41 +00:00
|
|
|
let redirectTo = homeLocation;
|
2018-04-13 14:48:10 +00:00
|
|
|
|
2018-08-29 19:52:41 +00:00
|
|
|
if (req.session && req.session.returnTo) {
|
2018-04-13 14:48:10 +00:00
|
|
|
redirectTo = req.session.returnTo;
|
|
|
|
}
|
|
|
|
|
|
|
|
req.flash('info', msg);
|
|
|
|
return res.redirect(redirectTo);
|
|
|
|
})
|
2017-12-27 18:11:17 +00:00
|
|
|
.subscribe(_.noop, next);
|
2017-12-26 21:20:03 +00:00
|
|
|
}
|
|
|
|
|
2017-12-27 18:11:17 +00:00
|
|
|
api.post(
|
|
|
|
'/passwordless-auth',
|
|
|
|
ifUserRedirect,
|
|
|
|
passwordlessPostValidators,
|
2018-08-29 19:52:41 +00:00
|
|
|
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
2017-12-27 18:11:17 +00:00
|
|
|
postPasswordlessAuth
|
|
|
|
);
|
2017-12-26 21:20:03 +00:00
|
|
|
|
|
|
|
app.use(api);
|
2015-06-03 00:27:02 +00:00
|
|
|
};
|