166 lines
5.2 KiB
JavaScript
166 lines
5.2 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
// heavily inspired from https://gock.net/blog/2020/nginx-subrequest-authentication-server/ and https://github.com/andygock/auth-server
|
||
|
|
|
||
|
|
exports = module.exports = {
|
||
|
|
start,
|
||
|
|
stop
|
||
|
|
};
|
||
|
|
|
||
|
|
const assert = require('assert'),
|
||
|
|
BoxError = require('./boxerror.js'),
|
||
|
|
constants = require('./constants.js'),
|
||
|
|
debug = require('debug')('box:authwall'),
|
||
|
|
express = require('express'),
|
||
|
|
http = require('http'),
|
||
|
|
HttpError = require('connect-lastmile').HttpError,
|
||
|
|
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||
|
|
jwt = require('jsonwebtoken'),
|
||
|
|
middleware = require('./middleware'),
|
||
|
|
mustacheExpress = require('mustache-express'),
|
||
|
|
path = require('path'),
|
||
|
|
users = require('./users.js');
|
||
|
|
|
||
|
|
let gHttpServer = null;
|
||
|
|
const TOKEN_SECRET = 'somerandomsecret';
|
||
|
|
const EXPIRY_DAYS = 7;
|
||
|
|
|
||
|
|
// middleware to check auth status
|
||
|
|
function jwtVerify(req, res, next) {
|
||
|
|
const token = req.cookies.authToken;
|
||
|
|
|
||
|
|
if (!token) return next();
|
||
|
|
|
||
|
|
jwt.verify(token, TOKEN_SECRET, function (error, decoded) {
|
||
|
|
if (error) {
|
||
|
|
debug('clearing token', error);
|
||
|
|
res.clearCookie('authToken');
|
||
|
|
return next(new HttpError(403, 'Malformed token or bad signature'));
|
||
|
|
}
|
||
|
|
|
||
|
|
req.user = decoded.user || null;
|
||
|
|
next();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function loginPage(req, res) {
|
||
|
|
const requestUri = req.headers['x-original-uri'];
|
||
|
|
const host = req.headers['x-original-host'];
|
||
|
|
|
||
|
|
return res.render('login', {
|
||
|
|
referer: requestUri ? `${host}/${requestUri}` : '/',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// called by nginx to authorize any protected route
|
||
|
|
function auth(req, res, next) {
|
||
|
|
if (!req.user) return next(new HttpError(401, 'Unauthorized'));
|
||
|
|
|
||
|
|
// user is already authenticated, refresh cookie
|
||
|
|
const token = jwt.sign({ user: req.user }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
|
||
|
|
|
||
|
|
res.cookie('authToken', token, {
|
||
|
|
httpOnly: true,
|
||
|
|
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
|
||
|
|
secure: true
|
||
|
|
});
|
||
|
|
|
||
|
|
return next(new HttpSuccess(200, {}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// endpoint called by login page, username and password posted as JSON body
|
||
|
|
function login(req, res, next) {
|
||
|
|
assert.strictEqual(typeof req.body, 'object');
|
||
|
|
|
||
|
|
const appId = req.headers['x-app-id'] || '';
|
||
|
|
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
|
||
|
|
|
||
|
|
if (typeof req.body.username !== 'string') return res.render('login', { error: 'username must be non empty string' });
|
||
|
|
if (typeof req.body.password !== 'string') return res.render('login', { error: 'password must be non empty string' });
|
||
|
|
|
||
|
|
const { username, password } = req.body;
|
||
|
|
|
||
|
|
const api = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername;
|
||
|
|
|
||
|
|
api(username, password, appId, function (error, user) {
|
||
|
|
if (error) return res.render('login', { error: 'Invalid username or password' });
|
||
|
|
|
||
|
|
const token = jwt.sign({ user: users.removePrivateFields(user) }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
|
||
|
|
|
||
|
|
res.cookie('authToken', token, {
|
||
|
|
httpOnly: true,
|
||
|
|
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
|
||
|
|
secure: true
|
||
|
|
});
|
||
|
|
|
||
|
|
res.redirect('/');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function logoutPage(req, res) {
|
||
|
|
res.clearCookie('authToken');
|
||
|
|
res.redirect('/login');
|
||
|
|
}
|
||
|
|
|
||
|
|
function logout(req, res, next) {
|
||
|
|
res.clearCookie('authToken');
|
||
|
|
next(new HttpSuccess(200, {}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// provides webhooks for the auth wall
|
||
|
|
function initializeAuthwallExpressSync() {
|
||
|
|
let app = express();
|
||
|
|
let httpServer = http.createServer(app);
|
||
|
|
|
||
|
|
let QUERY_LIMIT = '1mb'; // max size for json and urlencoded queries
|
||
|
|
let REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||
|
|
|
||
|
|
let json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
|
||
|
|
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
|
||
|
|
|
||
|
|
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Authwall :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||
|
|
|
||
|
|
var router = new express.Router();
|
||
|
|
router.del = router.delete; // amend router.del for readability further on
|
||
|
|
|
||
|
|
app.engine('html', mustacheExpress());
|
||
|
|
app.set('views', path.join(__dirname, 'authwall'));
|
||
|
|
app.set('view engine', 'html');
|
||
|
|
|
||
|
|
app
|
||
|
|
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||
|
|
.use(middleware.cookieParser())
|
||
|
|
.use(json)
|
||
|
|
.use(urlencoded)
|
||
|
|
.use(jwtVerify)
|
||
|
|
.use(router)
|
||
|
|
.use(middleware.lastMile());
|
||
|
|
|
||
|
|
router.get ('/', (req, res) => { res.redirect('/login'); });
|
||
|
|
router.get ('/login', loginPage);
|
||
|
|
router.get ('/auth', auth);
|
||
|
|
router.post('/login', login);
|
||
|
|
router.get ('/logout', logoutPage);
|
||
|
|
router.post('/logout', logout);
|
||
|
|
|
||
|
|
return httpServer;
|
||
|
|
}
|
||
|
|
|
||
|
|
function start(callback) {
|
||
|
|
assert.strictEqual(typeof callback, 'function');
|
||
|
|
assert.strictEqual(gHttpServer, null, 'Authwall is already up and running.');
|
||
|
|
|
||
|
|
gHttpServer = initializeAuthwallExpressSync();
|
||
|
|
|
||
|
|
gHttpServer.listen(constants.AUTHWALL_PORT, '127.0.0.1', callback);
|
||
|
|
}
|
||
|
|
|
||
|
|
function stop(callback) {
|
||
|
|
assert.strictEqual(typeof callback, 'function');
|
||
|
|
|
||
|
|
if (!gHttpServer) return callback(null);
|
||
|
|
|
||
|
|
gHttpServer.close(callback);
|
||
|
|
gHttpServer = null;
|
||
|
|
}
|