// auth.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const express = require('express'); const mongoose = require('mongoose'); const passport = require('passport'); const uuidv4 = require('uuid').v4; const { SiteController, SiteError } = require('../../lib/site-lib'); const ConnectToken = mongoose.model('ConnectToken'); class AuthController extends SiteController { constructor (dtp) { super(dtp, module.exports); } async start ( ) { const { coreNode: coreNodeService, limiter: limiterService, } = this.dtp.services; const upload = this.createMulter(); const router = express.Router(); this.dtp.app.use('/auth', router); const authRequired = this.dtp.services.session.authCheckMiddleware({ requireLogin: true }); const authRequiredNoRedirect = this.dtp.services.session.authCheckMiddleware({ requireLogin: true, useRedirect: false }); router.post( '/otp/enable', limiterService.createMiddleware(limiterService.config.auth.postOtpEnable), this.postOtpEnable.bind(this), ); router.post( '/otp/auth', limiterService.createMiddleware(limiterService.config.auth.postOtpAuthenticate), this.postOtpAuthenticate.bind(this), ); router.post( '/login', limiterService.createMiddleware(limiterService.config.auth.postLogin), upload.none(), this.postLogin.bind(this), ); router.get( '/api-token/personal', authRequired, limiterService.createMiddleware(limiterService.config.auth.getPersonalApiToken), this.getPersonalApiToken.bind(this), ); router.get( '/socket-token', authRequiredNoRedirect, limiterService.createMiddleware(limiterService.config.auth.getSocketToken), this.getSocketToken.bind(this), ); await coreNodeService.attachExpressRoutes(router); router.get( '/core', limiterService.createMiddleware(limiterService.config.auth.getCoreHome), this.getCoreHome.bind(this), ); router.get( '/logout', authRequired, limiterService.createMiddleware(limiterService.config.auth.getLogout), this.getLogout.bind(this), ); return router; } async postOtpEnable (req, res, next) { const { logan: loganService, otpAuth: otpAuthService, } = this.dtp.services; const service = req.body['otp-service']; const secret = req.body['otp-secret']; const token = req.body['otp-token']; const otpRedirectURL = req.body['otp-redirect'] || '/'; try { this.log.info('enabling OTP protections', { service, secret, token }); res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token); res.locals.otpRedirectURL = otpRedirectURL; loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postOtpEnable', data: { user: { _id: req.user._id, username: req.user.username, }, }, }); res.render('otp/new-account'); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postOtpEnable', message: `failed to enable OTP account: ${error.message}`, data: { service, error }, }); return next(error); } } async postOtpAuthenticate (req, res, next) { const { logan: loganService, otpAuth: otpAuthService, } = this.dtp.services; if (!req.user) { return res.status(403).json({ success: false, message: 'Must be logged in', }); } const service = req.body['otp-service']; if (!service) { return res.status(400).json({ success: false, message: 'Must specify OTP service name', }); } const passcode = req.body['otp-passcode']; if (!passcode || (typeof passcode !== 'string') || (passcode.length !== 6)) { return res.status(400).json({ success: false, message: 'Must include a valid passcode', }); } try { await otpAuthService.startOtpSession(req, service, passcode); loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postOtpAuthenticate', data: { user: { _id: req.user._id, username: req.user.username, }, }, }); return res.redirect(req.body['otp-redirect']); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postOtpAuthenticate', message: `failed to verify one-time password: ${error.message}`, data: { user: { _id: req.user._id, username: req.user.username, }, error, }, }); return next(error); } } async postLogin (req, res, next) { const { logan: loganService } = this.dtp.services; const redirectUri = req.session.loginReturnTo || '/'; this.log.debug('starting passport.authenticate', { redirectUri }); passport.authenticate('dtp-local', (error, user/*, info*/) => { if (error) { req.session.loginResult = error.toString(); loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postLogin', message: `login failed: ${error.message}`, data: { error }, }); return next(error); } if (!user) { req.session.loginResult = 'Username or email address is unknown.'; loganService.sendRequestEvent(module.exports, req, { level: 'alert', event: 'postLogin', message: 'username or email address is unknown', }); return res.redirect('/welcome/login'); } this.log.info('user logging in', { user: user.username }); req.login(user, async (error) => { if (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'postLogin', message: `failed to start user session: ${error.message}`, data: { error }, }); return next(error); } // scrub login return URL from session delete req.session.loginReturnTo; loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'postLogin', message: 'session started for site member', data: { user: { _id: user._id, username: user.username, }, }, }); // redirect to whatever was wanted return res.redirect(redirectUri); }); })(req, res, next); } async getPersonalApiToken (req, res, next) { const { apiGuard: apiGuardService, logan: loganService, } = this.dtp.platform.services; try { res.locals.apiToken = await apiGuardService.createApiToken(req.user, [ 'account-read', // additional scopes go here ]); res.render('api-token/view'); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getPersonalApiToken', message: `failed to generate API token: ${error.message}`, data: { error }, }); return next(error); } } async getSocketToken (req, res, next) { const { logan: loganService } = this.dtp.services; try { const token = await ConnectToken.create({ created: new Date(), userType: req.user.core ? 'CoreUser' : 'User', user: req.user._id, token: uuidv4(), }); res.status(200).json({ success: true, token: token.token }); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getSocketToken', message: `failed to create socket token: ${error.message}`, data: { error }, }); return next(error); } } async getCoreHome (req, res, next) { const { coreNode: coreNodeService, logan: loganService, } = this.dtp.services; try { res.locals.currentView = 'welcome'; res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.connectedCores = await coreNodeService.getConnectedCores(res.locals.pagination); res.render('welcome/core-home'); } catch (error) { loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getCoreHome', message: `failed to render view: ${error.message}`, data: { error }, }); return next(error); } } async getLogout (req, res, next) { const { logan: loganService } = this.dtp.services; if (!req.user) { return next(new SiteError(403, 'You are not signed in')); } const user = { _id: req.user._id, username: req.user.username, }; req.logout(); req.session.destroy((err) => { if (err) { this.log.error('failed to destroy browser session', { err }); loganService.sendRequestEvent(module.exports, req, { level: 'error', event: 'getLogout', message: 'failed to destroy browser session', data: { error: err }, }); return next(err); } loganService.sendRequestEvent(module.exports, req, { level: 'info', event: 'getLogout', message: 'user terminated their browser session', data: { user }, }); res.redirect('/'); }); } } module.exports = { logId: 'auth', index: 'auth', className: 'AuthController', create: async (dtp) => { return new AuthController(dtp); }, };