2018-10-23 23:24:48 +00:00
|
|
|
import passport from 'passport';
|
2019-09-09 21:43:51 +00:00
|
|
|
import dedent from 'dedent';
|
|
|
|
import { check } from 'express-validator/check';
|
|
|
|
import { isEmail } from 'validator';
|
2017-12-27 18:11:17 +00:00
|
|
|
|
2018-08-31 15:04:04 +00:00
|
|
|
import { homeLocation } from '../../../config/env';
|
2019-02-16 00:31:05 +00:00
|
|
|
import {
|
2018-10-30 21:17:07 +00:00
|
|
|
createPassportCallbackAuthenticator,
|
|
|
|
saveResponseAuthCookies,
|
|
|
|
loginRedirect
|
|
|
|
} from '../component-passport';
|
2019-09-09 21:43:51 +00:00
|
|
|
import { ifUserRedirectTo, ifNoUserRedirectTo } from '../utils/middleware';
|
2018-01-23 01:08:33 +00:00
|
|
|
import { wrapHandledError } from '../utils/create-handled-error.js';
|
2019-02-20 23:07:12 +00:00
|
|
|
import { removeCookies } from '../utils/getSetAccessToken';
|
2019-09-09 21:43:51 +00:00
|
|
|
import { decodeEmail } from '../../common/utils';
|
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
|
|
|
|
2019-09-09 21:43:51 +00:00
|
|
|
const passwordlessGetValidators = [
|
|
|
|
check('email')
|
|
|
|
.isBase64()
|
|
|
|
.withMessage('Email should be a base64 encoded string.'),
|
|
|
|
check('token')
|
|
|
|
.exists()
|
|
|
|
.withMessage('Token should exist.')
|
|
|
|
// based on strongloop/loopback/common/models/access-token.js#L15
|
|
|
|
.isLength({ min: 64, max: 64 })
|
|
|
|
.withMessage('Token is not the right length.')
|
|
|
|
];
|
|
|
|
|
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();
|
2019-09-09 21:43:51 +00:00
|
|
|
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
|
2018-10-30 21:17:07 +00:00
|
|
|
const saveAuthCookies = saveResponseAuthCookies();
|
|
|
|
const loginSuccessRedirect = loginRedirect();
|
2017-12-26 21:20:03 +00:00
|
|
|
const api = app.loopback.Router();
|
|
|
|
|
2018-10-30 21:17:07 +00:00
|
|
|
// Use a local mock strategy for signing in if we are in dev mode.
|
|
|
|
// Otherwise we use auth0 login. We use a string for 'true' because values
|
|
|
|
// set in the env file will always be strings and never boolean.
|
|
|
|
if (process.env.LOCAL_MOCK_AUTH === 'true') {
|
|
|
|
api.get(
|
|
|
|
'/signin',
|
|
|
|
passport.authenticate('devlogin'),
|
|
|
|
saveAuthCookies,
|
|
|
|
loginSuccessRedirect
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
api.get(
|
|
|
|
'/signin',
|
2019-10-21 11:33:00 +00:00
|
|
|
(req, res, next) => {
|
|
|
|
if (req && req.query && req.query.returnTo) {
|
|
|
|
req.query.returnTo = `${homeLocation}/${req.query.returnTo}`;
|
|
|
|
}
|
|
|
|
return next();
|
|
|
|
},
|
2018-10-30 21:17:07 +00:00
|
|
|
ifUserRedirect,
|
2019-10-21 11:33:00 +00:00
|
|
|
(req, res, next) => {
|
|
|
|
const state = req.query.returnTo
|
|
|
|
? Buffer.from(req.query.returnTo).toString('base64')
|
|
|
|
: null;
|
|
|
|
return passport.authenticate('auth0-login', { state })(req, res, next);
|
|
|
|
}
|
2018-10-30 21:17:07 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
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',
|
2019-06-19 14:31:03 +00:00
|
|
|
message: 'We could not log you out, please try again in a moment.',
|
2018-08-29 19:52:41 +00:00
|
|
|
redirectTo: homeLocation
|
|
|
|
});
|
2018-05-25 17:44:09 +00:00
|
|
|
}
|
2019-02-20 23:07:12 +00:00
|
|
|
removeCookies(req, res);
|
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
|
|
|
|
2019-09-09 21:43:51 +00:00
|
|
|
api.get(
|
|
|
|
'/confirm-email',
|
|
|
|
ifNoUserRedirectHome,
|
|
|
|
passwordlessGetValidators,
|
|
|
|
createGetPasswordlessAuth(app)
|
|
|
|
);
|
|
|
|
|
2017-12-26 21:20:03 +00:00
|
|
|
app.use(api);
|
2015-06-03 00:27:02 +00:00
|
|
|
};
|
2019-09-09 21:43:51 +00:00
|
|
|
|
|
|
|
const defaultErrorMsg = dedent`
|
|
|
|
Oops, something is not right,
|
|
|
|
please request a fresh link to sign in / sign up.
|
|
|
|
`;
|
|
|
|
|
|
|
|
function createGetPasswordlessAuth(app) {
|
|
|
|
const {
|
|
|
|
models: { AuthToken, User }
|
|
|
|
} = app;
|
|
|
|
return function getPasswordlessAuth(req, res, next) {
|
|
|
|
const {
|
|
|
|
query: { email: encodedEmail, token: authTokenId, emailChange } = {}
|
|
|
|
} = req;
|
|
|
|
|
|
|
|
const email = decodeEmail(encodedEmail);
|
|
|
|
if (!isEmail(email)) {
|
|
|
|
return next(
|
|
|
|
wrapHandledError(new TypeError('decoded email is invalid'), {
|
|
|
|
type: 'info',
|
|
|
|
message: 'The email encoded in the link is incorrectly formatted',
|
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// first find
|
|
|
|
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) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error(`no user found for token: ${authTokenId}`),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (user.email !== email) {
|
|
|
|
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
|
|
|
throw wrapHandledError(
|
|
|
|
new Error('user email does not match'),
|
|
|
|
{
|
|
|
|
type: 'info',
|
|
|
|
message: defaultErrorMsg,
|
|
|
|
redirectTo: `${homeLocation}/signin`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return authToken
|
|
|
|
.validate$()
|
|
|
|
.map(isValid => {
|
|
|
|
if (!isValid) {
|
|
|
|
throw wrapHandledError(new Error('token is invalid'), {
|
|
|
|
type: 'info',
|
|
|
|
message: `
|
|
|
|
Looks like the link you clicked has expired,
|
|
|
|
please request a fresh link, to sign in.
|
|
|
|
`,
|
|
|
|
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!'
|
|
|
|
);
|
2019-10-08 15:15:36 +00:00
|
|
|
return res.redirectWithFlash(`${homeLocation}/learn`);
|
2019-09-09 21:43:51 +00:00
|
|
|
})
|
|
|
|
.subscribe(() => {}, next)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|