You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
9.6 KiB
346 lines
9.6 KiB
// 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 = {
|
|
slug: 'auth',
|
|
name: 'auth',
|
|
className: 'AuthController',
|
|
create: async (dtp) => { return new AuthController(dtp); },
|
|
};
|