200 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			200 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import passport from 'passport';
 | 
						|
import dedent from 'dedent';
 | 
						|
import { check } from 'express-validator/check';
 | 
						|
import { isEmail } from 'validator';
 | 
						|
 | 
						|
import { homeLocation } from '../../../config/env';
 | 
						|
import {
 | 
						|
  createPassportCallbackAuthenticator,
 | 
						|
  saveResponseAuthCookies,
 | 
						|
  loginRedirect
 | 
						|
} from '../component-passport';
 | 
						|
import { ifUserRedirectTo, ifNoUserRedirectTo } from '../utils/middleware';
 | 
						|
import { wrapHandledError } from '../utils/create-handled-error.js';
 | 
						|
import { removeCookies } from '../utils/getSetAccessToken';
 | 
						|
import { decodeEmail } from '../../common/utils';
 | 
						|
 | 
						|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
 | 
						|
if (isSignUpDisabled) {
 | 
						|
  console.log('fcc:boot:auth - Sign up is disabled');
 | 
						|
}
 | 
						|
 | 
						|
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.')
 | 
						|
];
 | 
						|
 | 
						|
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 saveAuthCookies = saveResponseAuthCookies();
 | 
						|
  const loginSuccessRedirect = loginRedirect();
 | 
						|
  const api = app.loopback.Router();
 | 
						|
 | 
						|
  // 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',
 | 
						|
      (req, res, next) => {
 | 
						|
        if (req && req.query && req.query.returnTo) {
 | 
						|
          req.query.returnTo = `${homeLocation}/${req.query.returnTo}`;
 | 
						|
        }
 | 
						|
        return next();
 | 
						|
      },
 | 
						|
      ifUserRedirect,
 | 
						|
      (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);
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
    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: 'We could not log you out, please try again in a moment.',
 | 
						|
          redirectTo: homeLocation
 | 
						|
        });
 | 
						|
      }
 | 
						|
      removeCookies(req, res);
 | 
						|
      res.redirect(homeLocation);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  api.get(
 | 
						|
    '/confirm-email',
 | 
						|
    ifNoUserRedirectHome,
 | 
						|
    passwordlessGetValidators,
 | 
						|
    createGetPasswordlessAuth(app)
 | 
						|
  );
 | 
						|
 | 
						|
  app.use(api);
 | 
						|
};
 | 
						|
 | 
						|
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!'
 | 
						|
          );
 | 
						|
          return res.redirectWithFlash(`${homeLocation}/learn`);
 | 
						|
        })
 | 
						|
        .subscribe(() => {}, next)
 | 
						|
    );
 | 
						|
  };
 | 
						|
}
 |