221 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import _ from 'lodash';
 | 
						|
import { Observable } from 'rx';
 | 
						|
import dedent from 'dedent';
 | 
						|
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,
 | 
						|
  createValidatorErrorHandler
 | 
						|
} from '../utils/middleware';
 | 
						|
import { wrapHandledError } from '../utils/create-handled-error.js';
 | 
						|
 | 
						|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
 | 
						|
if (isSignUpDisabled) {
 | 
						|
  console.log('fcc:boot:auth - Sign up is disabled');
 | 
						|
}
 | 
						|
 | 
						|
module.exports = function enableAuthentication(app) {
 | 
						|
  // enable loopback access control authentication. see:
 | 
						|
  // loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
 | 
						|
  app.enableAuth();
 | 
						|
  const ifUserRedirect = ifUserRedirectTo();
 | 
						|
  const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
 | 
						|
  const api = app.loopback.Router();
 | 
						|
  const { AuthToken, User } = app.models;
 | 
						|
 | 
						|
  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();
 | 
						|
    req.session.destroy(err => {
 | 
						|
      if (err) {
 | 
						|
        throw wrapHandledError(new Error('could not destroy session'), {
 | 
						|
          type: 'info',
 | 
						|
          message: 'Oops, something is not right.',
 | 
						|
          redirectTo: homeLocation
 | 
						|
        });
 | 
						|
      }
 | 
						|
      const config = createCookieConfig(req);
 | 
						|
      res.clearCookie('jwt_access_token', config);
 | 
						|
      res.clearCookie('access_token', config);
 | 
						|
      res.clearCookie('userId', config);
 | 
						|
      res.clearCookie('_csrf', config);
 | 
						|
      res.redirect(homeLocation);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  const defaultErrorMsg = dedent`
 | 
						|
    Oops, something is not right,
 | 
						|
    please request a fresh link to sign in / sign up.
 | 
						|
  `;
 | 
						|
 | 
						|
  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.')
 | 
						|
  ];
 | 
						|
 | 
						|
  function getPasswordlessAuth(req, res, next) {
 | 
						|
    const {
 | 
						|
      query: { email: encodedEmail, token: authTokenId, emailChange } = {}
 | 
						|
    } = req;
 | 
						|
 | 
						|
    const email = User.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!'
 | 
						|
          );
 | 
						|
          return res.redirectWithFlash(`${homeLocation}/welcome`);
 | 
						|
        })
 | 
						|
        .subscribe(() => {}, next)
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  api.get(
 | 
						|
    '/passwordless-auth',
 | 
						|
    ifUserRedirect,
 | 
						|
    passwordlessGetValidators,
 | 
						|
    createValidatorErrorHandler('errors', `${homeLocation}/signin`),
 | 
						|
    getPasswordlessAuth
 | 
						|
  );
 | 
						|
 | 
						|
  api.get('/passwordless-change', (req, res) =>
 | 
						|
    res.redirect(301, '/confirm-email')
 | 
						|
  );
 | 
						|
 | 
						|
  api.get(
 | 
						|
    '/confirm-email',
 | 
						|
    ifNoUserRedirectHome,
 | 
						|
    passwordlessGetValidators,
 | 
						|
    getPasswordlessAuth
 | 
						|
  );
 | 
						|
 | 
						|
  const passwordlessPostValidators = [
 | 
						|
    check('email')
 | 
						|
      .isEmail()
 | 
						|
      .withMessage('Email is not a valid email address.')
 | 
						|
  ];
 | 
						|
  function postPasswordlessAuth(req, res, next) {
 | 
						|
    const { body: { email } = {} } = req;
 | 
						|
 | 
						|
    return User.findOne$({ where: { email } })
 | 
						|
      .flatMap(_user =>
 | 
						|
        Observable.if(
 | 
						|
          // if no user found create new user and save to db
 | 
						|
          _.constant(_user),
 | 
						|
          Observable.of(_user),
 | 
						|
          User.create$({ email })
 | 
						|
        ).flatMap(user => user.requestAuthEmail(!_user))
 | 
						|
      )
 | 
						|
      .do(msg => {
 | 
						|
        let redirectTo = homeLocation;
 | 
						|
 | 
						|
        if (req.session && req.session.returnTo) {
 | 
						|
          redirectTo = req.session.returnTo;
 | 
						|
        }
 | 
						|
 | 
						|
        req.flash('info', msg);
 | 
						|
        return res.redirect(redirectTo);
 | 
						|
      })
 | 
						|
      .subscribe(_.noop, next);
 | 
						|
  }
 | 
						|
 | 
						|
  api.post(
 | 
						|
    '/passwordless-auth',
 | 
						|
    ifUserRedirect,
 | 
						|
    passwordlessPostValidators,
 | 
						|
    createValidatorErrorHandler('errors', `${homeLocation}/signin`),
 | 
						|
    postPasswordlessAuth
 | 
						|
  );
 | 
						|
 | 
						|
  app.use(api);
 | 
						|
};
 |