2020-11-09 20:34:48 -08:00
'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
} ;
2020-11-11 00:21:51 -08:00
const apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2020-11-11 13:25:52 -08:00
basicAuth = require ( 'basic-auth' ) ,
2020-11-09 20:34:48 -08:00
constants = require ( './constants.js' ) ,
2020-11-10 09:59:28 -08:00
debug = require ( 'debug' ) ( 'box:proxyAuth' ) ,
2020-11-24 20:57:13 +01:00
ejs = require ( 'ejs' ) ,
2020-11-09 20:34:48 -08:00
express = require ( 'express' ) ,
2020-11-10 17:10:57 -08:00
fs = require ( 'fs' ) ,
hat = require ( './hat.js' ) ,
2020-11-09 20:34:48 -08:00
http = require ( 'http' ) ,
HttpError = require ( 'connect-lastmile' ) . HttpError ,
HttpSuccess = require ( 'connect-lastmile' ) . HttpSuccess ,
jwt = require ( 'jsonwebtoken' ) ,
middleware = require ( './middleware' ) ,
path = require ( 'path' ) ,
2020-11-10 17:10:57 -08:00
paths = require ( './paths.js' ) ,
2020-11-11 00:21:51 -08:00
safe = require ( 'safetydance' ) ,
2020-12-20 13:13:36 -08:00
speakeasy = require ( 'speakeasy' ) ,
2020-11-24 20:57:13 +01:00
translation = require ( './translation.js' ) ,
2020-11-09 20:34:48 -08:00
users = require ( './users.js' ) ;
let gHttpServer = null ;
2020-11-10 17:10:57 -08:00
let TOKEN _SECRET = null ;
2020-11-09 20:34:48 -08:00
const EXPIRY _DAYS = 7 ;
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 ( ) ;
} ) ;
}
2020-11-11 13:25:52 -08:00
function basicAuthVerify ( req , res , next ) {
const appId = req . headers [ 'x-app-id' ] || '' ;
const credentials = basicAuth ( req ) ;
if ( ! appId || ! credentials ) return next ( ) ;
const api = credentials . name . indexOf ( '@' ) !== - 1 ? users . verifyWithEmail : users . verifyWithUsername ;
2021-02-23 21:52:58 -08:00
apps . get ( appId , function ( error , app ) {
if ( error ) return next ( new HttpError ( 503 , error . message ) ) ;
2020-11-11 13:25:52 -08:00
2021-02-23 21:52:58 -08:00
if ( ! app . manifest . addons . proxyAuth . basicAuth ) return next ( ) ;
api ( credentials . name , credentials . pass , appId , function ( error , user ) {
if ( error ) return next ( new HttpError ( 403 , 'Invalid username or password' ) ) ;
req . user = user ;
next ( ) ;
} ) ;
2020-11-11 13:25:52 -08:00
} ) ;
}
2020-11-11 00:21:51 -08:00
function loginPage ( req , res , next ) {
const appId = req . headers [ 'x-app-id' ] || '' ;
if ( ! appId ) return next ( new HttpError ( 503 , 'Nginx misconfiguration' ) ) ;
2020-11-24 20:57:13 +01:00
translation . getTranslations ( function ( error , translationAssets ) {
if ( error ) return next ( new HttpError ( 500 , 'No translation found' ) ) ;
const raw = safe . fs . readFileSync ( path . join ( paths . DASHBOARD _DIR , 'templates/proxyauth-login.ejs' ) , 'utf8' ) ;
if ( raw === null ) return next ( new HttpError ( 500 , 'Login template not found' ) ) ;
const translatedContent = translation . translate ( raw , translationAssets . translations || { } , translationAssets . fallback || { } ) ;
var finalContent = '' ;
apps . get ( appId , function ( error , app ) {
if ( error ) return next ( new HttpError ( 503 , error . message ) ) ;
2020-11-11 00:21:51 -08:00
2020-11-24 20:57:13 +01:00
const title = app . label || app . manifest . title ;
2020-11-11 00:21:51 -08:00
2020-11-24 20:57:13 +01:00
apps . getIconPath ( app , { } , function ( error , iconPath ) {
const icon = 'data:image/png;base64,' + safe . fs . readFileSync ( iconPath || '' , 'base64' ) ;
2020-11-11 00:21:51 -08:00
2020-11-24 20:57:13 +01:00
try {
finalContent = ejs . render ( translatedContent , { title , icon } ) ;
} catch ( e ) {
debug ( 'Error rendering proxyauth-login.ejs' , e ) ;
return next ( new HttpError ( 500 , 'Login template error' ) ) ;
}
res . set ( 'Content-Type' , 'text/html' ) ;
return res . send ( finalContent ) ;
} ) ;
2020-11-11 00:21:51 -08:00
} ) ;
} ) ;
2020-11-09 20:34:48 -08:00
}
2021-01-26 23:25:56 -08:00
// someday this can be more sophisticated and check for a real browser
function isBrowser ( req ) {
const userAgent = req . get ( 'user-agent' ) ;
if ( ! userAgent ) return false ;
2021-02-09 13:44:34 -08:00
// https://github.com/docker/engine/blob/master/dockerversion/useragent.go#L18
return ! userAgent . toLowerCase ( ) . includes ( 'docker' ) ;
2021-01-26 23:25:56 -08:00
}
// called by nginx to authorize any protected route. this route must return only 2xx or 401/403 (http://nginx.org/en/docs/http/ngx_http_auth_request_module.html)
2020-11-09 20:34:48 -08:00
function auth ( req , res , next ) {
2021-01-26 23:25:56 -08:00
if ( ! req . user ) {
if ( isBrowser ( req ) ) return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
// the header has to be generated here and cannot be set in nginx config - https://forum.nginx.org/read.php?2,171461,171469#msg-171469
res . set ( 'www-authenticate' , 'Basic realm="Cloudron"' ) ;
return next ( new HttpError ( 401 , 'Unauthorized' ) ) ;
}
2020-11-09 20:34:48 -08:00
// 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
2020-12-20 13:13:36 -08:00
function passwordAuth ( req , res , next ) {
2020-11-09 20:34:48 -08:00
assert . strictEqual ( typeof req . body , 'object' ) ;
const appId = req . headers [ 'x-app-id' ] || '' ;
if ( ! appId ) return next ( new HttpError ( 503 , 'Nginx misconfiguration' ) ) ;
2020-11-10 20:04:31 -08:00
if ( typeof req . body . username !== 'string' ) return next ( new HttpError ( 400 , 'username must be non empty string' ) ) ;
if ( typeof req . body . password !== 'string' ) return next ( new HttpError ( 400 , 'password must be non empty string' ) ) ;
2020-12-20 14:41:16 -08:00
if ( 'totpToken' in req . body && typeof req . body . totpToken !== 'string' ) return next ( new HttpError ( 400 , 'totpToken must be a string' ) ) ;
2020-11-09 20:34:48 -08:00
2020-12-20 14:41:16 -08:00
const { username , password , totpToken } = req . body ;
2020-11-09 20:34:48 -08:00
const api = username . indexOf ( '@' ) !== - 1 ? users . verifyWithEmail : users . verifyWithUsername ;
api ( username , password , appId , function ( error , user ) {
2020-11-10 20:04:31 -08:00
if ( error ) return next ( new HttpError ( 403 , 'Invalid username or password' ) ) ;
2020-11-09 20:34:48 -08:00
2020-12-20 13:13:36 -08:00
if ( ! user . ghost && ! user . appPassword && user . twoFactorAuthenticationEnabled ) {
if ( ! totpToken ) return next ( new HttpError ( 403 , 'A totpToken must be provided' ) ) ;
let verified = speakeasy . totp . verify ( { secret : user . twoFactorAuthenticationSecret , encoding : 'base32' , token : req . body . totpToken , window : 2 } ) ;
if ( ! verified ) return next ( new HttpError ( 403 , 'Invalid totpToken' ) ) ;
}
2020-11-20 17:54:17 -08:00
req . user = user ;
next ( ) ;
} ) ;
}
2020-11-09 20:34:48 -08:00
2020-11-20 17:54:17 -08:00
function authorize ( req , res , next ) {
const appId = req . headers [ 'x-app-id' ] || '' ;
if ( ! appId ) return next ( new HttpError ( 503 , 'Nginx misconfiguration' ) ) ;
2020-11-09 20:34:48 -08:00
2020-11-20 17:54:17 -08:00
apps . get ( appId , function ( error , app ) {
if ( error ) return next ( new HttpError ( 403 , 'No such app' ) ) ;
apps . hasAccessTo ( app , req . user , function ( error , hasAccess ) {
if ( error ) return next ( new HttpError ( 403 , 'Forbidden' ) ) ;
if ( ! hasAccess ) return next ( new HttpError ( 403 , 'Forbidden' ) ) ;
const token = jwt . sign ( { user : users . removePrivateFields ( req . user ) } , TOKEN _SECRET , { expiresIn : ` ${ EXPIRY _DAYS } d ` } ) ;
res . cookie ( 'authToken' , token , {
httpOnly : true ,
maxAge : EXPIRY _DAYS * 86400 * 1000 , // milliseconds
secure : true
} ) ;
2020-12-13 13:24:59 -08:00
res . redirect ( 302 , '/' ) ;
2020-11-20 17:54:17 -08:00
} ) ;
2020-11-09 20:34:48 -08:00
} ) ;
}
2020-12-19 12:30:06 -08:00
function logoutPage ( req , res , next ) {
const appId = req . headers [ 'x-app-id' ] || '' ;
if ( ! appId ) return next ( new HttpError ( 503 , 'Nginx misconfiguration' ) ) ;
apps . get ( appId , function ( error , app ) {
if ( error ) return next ( new HttpError ( 503 , error . message ) ) ;
res . clearCookie ( 'authToken' ) ;
// when we have no path, redirect to the login page. we cannot redirect to '/' because browsers will immediately serve up the cached page
// if a path is set, we can assume '/' is a public page
res . redirect ( 302 , app . manifest . addons . proxyAuth . path ? '/' : '/login' ) ;
} ) ;
2020-11-09 20:34:48 -08:00
}
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
2020-11-11 13:25:52 -08:00
let json = middleware . json ( { strict : true , limit : QUERY _LIMIT } ) ; // application/json
2020-11-09 20:34:48 -08:00
2020-11-10 19:49:57 -08:00
if ( process . env . BOX _ENV !== 'test' ) app . use ( middleware . morgan ( 'proxyauth :method :url :status :response-time ms - :res[content-length]' , { immediate : false } ) ) ;
2020-11-09 20:34:48 -08:00
var router = new express . Router ( ) ;
router . del = router . delete ; // amend router.del for readability further on
app
. use ( middleware . timeout ( REQUEST _TIMEOUT ) )
. use ( middleware . cookieParser ( ) )
. use ( router )
. use ( middleware . lastMile ( ) ) ;
router . get ( '/login' , loginPage ) ;
2020-11-11 13:25:52 -08:00
router . get ( '/auth' , jwtVerify , basicAuthVerify , auth ) ;
2020-12-20 13:13:36 -08:00
router . post ( '/login' , json , passwordAuth , authorize ) ;
2020-11-09 20:34:48 -08:00
router . get ( '/logout' , logoutPage ) ;
2020-11-11 13:25:52 -08:00
router . post ( '/logout' , json , logout ) ;
2020-11-09 20:34:48 -08:00
return httpServer ;
}
function start ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
assert . strictEqual ( gHttpServer , null , 'Authwall is already up and running.' ) ;
2020-11-10 17:10:57 -08:00
if ( ! fs . existsSync ( paths . PROXY _AUTH _TOKEN _SECRET _FILE ) ) {
TOKEN _SECRET = hat ( 64 ) ;
fs . writeFileSync ( paths . PROXY _AUTH _TOKEN _SECRET _FILE , TOKEN _SECRET , 'utf8' ) ;
} else {
TOKEN _SECRET = fs . readFileSync ( paths . PROXY _AUTH _TOKEN _SECRET _FILE , 'utf8' ) . trim ( ) ;
}
2020-11-09 20:34:48 -08:00
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 ;
}