'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; }