X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fauthenticator.js;h=c69ada91c6ed6a5234942b1e6ae8eb92a420d7e6;hb=HEAD;hp=1cff2108f309e2ac51245070281e0ec346f6168c;hpb=c13db4d55a8cf25c109dfcbb88a2d7828b791147;p=squeep-authentication-module diff --git a/lib/authenticator.js b/lib/authenticator.js index 1cff210..3df0b9d 100644 --- a/lib/authenticator.js +++ b/lib/authenticator.js @@ -4,91 +4,295 @@ const common = require('./common'); const Enum = require('./enum'); const Errors = require('./errors'); const { MysteryBox } = require('@squeep/mystery-box'); +const { TOTP } = require('@squeep/totp'); const { name: packageName } = require('../package'); const _fileScope = common.fileScope(__filename); +/** + * Wrangles the fiddly bits of authentication. + * Handles checking of credentials and otp codes, creating and validating + * cookie-based sessions, Basic auth. + * Interacts with the authentication database interface. + */ + +/** + * @typedef {import('node:http')} http + */ + class Authenticator { /** - * @param {Console} logger - * @param {*} db - * @param {(dbCtx: any, identifier: String) => Promise } db.authenticationGet - * @param {(dbCtx: any, identifier: String) => Promise} db.authenticationSuccess - * @param {((dbCtx: any) => Promise) => Promise} db.context - * @param {Object} options - * @param {Object} options.authenticator - * @param {Boolean} options.authenticator.secureAuthOnly - * @param {String[]} options.authenticator.forbiddenPAMIdentifiers - * @param {String[]} options.authenticator.authnEnabled - * @param {String[]=} options.authenticator.loginBlurb - * @param {String[]=} options.authenticator.indieAuthBlurb - * @param {String[]=} options.authenticator.userBlurb + * @typedef {object} AuthInfo + * @property {string} identifier identifier + * @property {string} credential hashed credential + * @property {string=} otpKey optional otp key + */ + /** + * @callback DBContextExec + * @param {object} dbCtx + * @returns {Promise} + */ + /** + * @typedef {object} AuthDBInterface + * @property {(DBContextExec) => Promise} context db context runner + * @property {(dbCtx: any, identifier: string) => Promise} authenticationGet get an authentication entry + * @property {(dbCtx: any, identifier: string) => Promise} authenticationSuccess store a successful authentication event + * @property {(dbCtx: any, identifier: string, credential: string, otpKey: string=) => Promise} authenticationUpsert update an authentication entry + * @property {(dbCtx: any, identifier: string, otpKey: string) => Promise} authenticationUpdateOTPKey update otp key + * @property {(dbCtx: any, identifier: string, credential: AuthInfo) => Promise} authenticationUpdateCredential update credential + */ + /** + * @typedef {object} ConsoleLike + * @property {Function} debug log debug + * @property {Function} error log error + * @property {Function} info log info + */ + /** + * @param {ConsoleLike} logger logger instance + * @param {AuthDBInterface} db db instance + * @param {object} options options + * @param {string | string[]} options.encryptionSecret encryption secret + * @param {object} options.authenticator authenticator options + * @param {boolean} options.authenticator.secureAuthOnly disable auth over non-https + * @param {string[]} options.authenticator.forbiddenPAMIdentifiers reject these identifiers for PAM auth + * @param {string[]} options.authenticator.authnEnabled in order of preference for storing new credentials + * @param {number=} options.authenticator.inactiveSessionLifespanSeconds session timeout + * @param {string[]=} options.authenticator.loginBlurb text for login page + * @param {string[]=} options.authenticator.indieAuthBlurb text for indieauth login section + * @param {string[]=} options.authenticator.userBlurb text for local user login section + * @param {string[]=} options.authenticator.otpBlurb text for otp entry + * @param {string=} options.dingus dingus options + * @param {string=} options.dingus.proxyPrefix base url prefix */ constructor(logger, db, options) { this.logger = logger; this.db = db; this.options = options; this.basicRealm = options.authenticator.basicRealm || packageName; - this.secureAuthOnly = options.authenticator.secureAuthOnly; + this.secureAuthOnly = options.authenticator.secureAuthOnly ?? true; + this.proxyPrefix = options.dingus?.proxyPrefix ?? ''; + // First construct map of all available code-supported auth mechanisms. this.authn = { - DEBUG_ANY: {}, indieAuth: {}, + plain: {}, }; try { this.authn.argon2 = require('argon2'); - } catch (e) { /**/ } + } catch (e) { /**/ } // eslint-disable-line no-unused-vars try { this.authn.pam = require('node-linux-pam'); - this.forbiddenPAMIdentifiers = options.authenticator.forbiddenPAMIdentifiers; - } catch (e) { /**/ } - - this.authnEnabled = Object.keys(this.authn).filter((auth) => options.authenticator.authnEnabled.includes(auth)); - this.logger.debug(_fileScope('constructor'), 'available mechanisms', { authn: this.authnEnabled }); + this.forbiddenPAMIdentifiers = options.authenticator.forbiddenPAMIdentifiers ?? ['root']; + } catch (e) { /**/ } // eslint-disable-line no-unused-vars + + // Track which authn methods we can change credentials et cetera. + const authnUpdatable = ['plain', 'argon2']; + + // Filter configured mechanisms from available, note the first as which to use for setting new credentials. + this.authnEnabled = new Set(); + this.authnPreferred = undefined; // For updating credentials + options.authenticator.authnEnabled.forEach((authn) => { + if (authn in this.authn) { + this.authnEnabled.add(authn); + if (!this.authnPreferred && authnUpdatable.includes(authn)) { + this.authnPreferred = authn; + } + } + }); + this.logger.debug(_fileScope('constructor'), 'available mechanisms', { authn: Array.from(this.authnEnabled), preferred: this.authnPreferred }); - if (this.authnEnabled.length === 0) { + if (this.authnEnabled.size === 0) { throw new Error('no authentication mechanisms available'); } - this.mysteryBox = new MysteryBox(logger, options); + this.mysteryBox = new MysteryBox(options); + this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name))); + + this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32; + + this.TOTP = TOTP; // For ease of test stubbing. + } + + + /** + * Populate the authentication database with a new identifier, the + * secured credential, and optionally an OTP key. + * @param {*} dbCtx db context + * @param {string} identifier identifier + * @param {string} credential plaintext + * @param {string=} otpKey otp key + * @returns {Promise} + */ + async createIdentifier(dbCtx, identifier, credential, otpKey = null) { + const _scope = _fileScope('createIdentifier'); + try { + const secureCredential = await this._secureCredential(credential); + await this.db.authenticationUpsert(dbCtx, identifier, secureCredential, otpKey); + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier }); + throw e; + } + } + + + /** + * Update the authentication database with a new secured credential + * for an indentifier. + * @param {*} dbCtx dbCtx + * @param {string} identifier identifier + * @param {string} credential plaintext + * @returns {Promise} + */ + async updateCredential(dbCtx, identifier, credential) { + const _scope = _fileScope('updateCredential'); + try { + const secureCredential = await this._secureCredential(credential); + await this.db.authenticationUpdateCredential(dbCtx, identifier, secureCredential); + this.logger.info(_scope, 'success', { identifier }); + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier }); + throw e; + } + } + + + /** + * Encode a plaintext credential in the preferred way to store in database. + * @param {string} credential plaintext + * @param {string=} authn authentication mechanism + * @returns {Promise} encoded credential + */ + async _secureCredential(credential, authn = this.authnPreferred) { + const _scope = _fileScope('_secureCredential'); + try { + switch (authn) { + case 'plain': + return '$plain$' + credential; + + case 'argon2': + return await this.authn.argon2.hash(credential, { type: this.authn.argon2.argon2id }); + + default: + throw new RangeError('Unknown authn'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, authn }); + throw e; + } + } + + + /** + * Checks a supplied credential against supplied data. + * @param {AuthInfo} authData from database + * @param {string} credential plaintext + * @returns {Promise} is valid + */ + async _validateAuthDataCredential(authData, credential) { + const _scope = _fileScope('_validateAuthDataCredential'); + + if (!authData?.credential) { + this.logger.debug(_scope, 'failed, no authInfo'); + return false; + } + if (authData.credential.startsWith('$argon2') + && this.authnEnabled.has('argon2')) { + return await this._isValidArgon2Identifier(authData, credential); + } + if (authData.credential.startsWith('$PAM$') + && this.authnEnabled.has('pam')) { + return await this._isValidPAMIdentifier(authData, credential); + } + if (authData.credential.startsWith('$plain$') + && this.authnEnabled.has('plain')) { + return this.constructor._isValidPlainIdentifier(authData, credential); + } + this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { authData }); + return false; + } + + + /** + * Check argon2. + * @param {AuthInfo} authData auth entry + * @param {string} credential to check + * @returns {Promise} is valid + */ + async _isValidArgon2Identifier(authData, credential) { + return await this.authn.argon2.verify(authData.credential, credential); + } + + + /** + * Check plaintext. + * @param {AuthInfo} authData auth entry + * @param {string} credential to check + * @returns {Promise} is valid + */ + static _isValidPlainIdentifier(authData, credential) { + return authData.credential.substring('$plain$'.length) === credential; + } + + + /** + * Check system PAM. + * @param {AuthInfo} authData auth entry + * @param {string} credential to check + * @returns {Promise} is valid + */ + async _isValidPAMIdentifier(authData, credential) { + const _scope = _fileScope('_isValidPAMIdentifier'); + const { identifier } = authData; + let isValid = false; + if (this.forbiddenPAMIdentifiers.includes(identifier)) { + return false; + } + try { + await this.authn.pam.pamAuthenticatePromise({ username: identifier, password: credential }); + isValid = true; + } catch (e) { + this.logger.debug(_scope, 'failed', { error: e }); + if (!(e instanceof this.authn.pam.PamError)) { + throw e; + } + } + return isValid; } /** * Check local auth entries. * Sets ctx.authenticatedId if valid. - * @param {String} identifier - * @param {String} credential - * @param {Object} ctx + * Sets ctx.otpKey if account has otpKey. + * @param {string} identifier identifier + * @param {string} credential to check + * @param {object} ctx context + * @returns {Promise} is valid */ async isValidIdentifierCredential(identifier, credential, ctx) { const _scope = _fileScope('isValidIdentifierCredential'); - this.logger.debug(_scope, 'called', { identifier, credential: '*'.repeat(credential.length), ctx }); + this.logger.debug(_scope, 'called', { identifier, credential: '*'.repeat((credential || '').length), ctx }); let isValid = false; + if (typeof credential === 'undefined') { + return isValid; + } + await this.db.context(async (dbCtx) => { const authData = await this.db.authenticationGet(dbCtx, identifier); if (!authData) { this.logger.debug(_scope, 'failed, invalid identifier', { ctx, identifier }); - } else { - if (authData.credential.startsWith('$argon2') - && this.authnEnabled.includes('argon2')) { - isValid = await this.authn.argon2.verify(authData.credential, credential); - } else if (authData.credential.startsWith('$PAM$') - && this.authnEnabled.includes('pam')) { - isValid = this._isValidPAMIdentifier(identifier, credential); - } else { - this.logger.error(_scope, 'failed, unknown type of stored credential', { identifier, ctx }); - } + return; } - if (this.authnEnabled.includes('DEBUG_ANY')) { - isValid = true; - } + isValid = await this._validateAuthDataCredential(authData, credential); if (isValid) { ctx.authenticationId = identifier; + if (authData.otpKey) { + ctx.otpKey = authData.otpKey; + } await this.db.authenticationSuccess(dbCtx, identifier); } }); // dbCtx @@ -98,40 +302,64 @@ class Authenticator { /** - * Check system PAM. - * @param {String} identifier - * @param {String} credential - * @returns {Boolean} + * @typedef {object} OTPState + * @property {string} key otp key + * @property {number} attempt count of attempts + * @property {number} epochMs when entry was initiated */ - async _isValidPAMIdentifier(identifier, credential) { - const _scope = _fileScope('_isValidPAMIdentifier'); - let isValid = false; - if (this.forbiddenPAMIdentifiers.includes(identifier)) { - return false; + /** + * Validate if an entered otp token matches the key. + * @param {OTPState} state otp state + * @param {string} otp to check + * @returns {Enum.OTPResult} result + */ + checkOTP(state, otp) { + const totp = new this.TOTP({ + keyEncoding: 'base32', + ...this.options?.authenticator?.otpOptions, + key: state.key, + }); + const isValid = totp.validate(otp); + if (isValid) { + return Enum.OTPResult.Valid; + } + const otpAttemptsRemain = state.attempt <= (this.options?.authenticator?.otpAttemptsAllowed ?? 3); + const otpWithinWindow = state.epochMs + (this.options?.authenticator?.otpWindowMs ?? 600000) > Date.now(); + if (otpAttemptsRemain && otpWithinWindow) { + return Enum.OTPResult.InvalidSoftFail; } + return Enum.OTPResult.InvalidHardFail; + } + + + /** + * Update the authentication database with a new otp key. + * @param {*} dbCtx db context + * @param {string} identifier identifier + * @param {string=} otpKey otp key + */ + async updateOTPKey(dbCtx, identifier, otpKey) { + const _scope = _fileScope('updateOTPKey'); try { - await this.authn.pam.pamAuthenticatePromise({ username: identifier, password: credential }); - isValid = true; + await this.db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey); + this.logger.info(_scope, 'otp key updated'); } catch (e) { - this.logger.debug(_scope, 'failed', { error: e }); - if (!(e instanceof this.authn.pam.PamError)) { - throw e; - } + this.logger.error(_scope, 'failed', { error: e, identifier }); } - return isValid; } /** * Check for valid Basic auth, updates ctx with identifier if valid. - * @param {String} credentials - * @param {Object} ctx - * @returns {Boolean} + * @param {string} authValue basic auth value (base64) + * @param {object} ctx context + * @returns {Promise} is valid */ - async isValidBasic(credentials, ctx) { + async isValidBasic(authValue, ctx) { const _scope = _fileScope('isValidBasic'); this.logger.debug(_scope, 'called', { ctx }); + const credentials = Buffer.from(authValue, 'base64').toString('utf-8'); const [identifier, credential] = common.splitFirst(credentials, ':', ''); return this.isValidIdentifierCredential(identifier, credential, ctx); @@ -140,8 +368,9 @@ class Authenticator { /** * Determine which Authorization header is available, and if it is valid. - * @param {String} authorizationHeader - * @param {Object} ctx + * @param {string} authorizationHeader request header + * @param {object} ctx context + * @returns {Promise} is valid */ async isValidAuthorization(authorizationHeader, ctx) { const _scope = _fileScope('isValidAuthorization'); @@ -151,8 +380,7 @@ class Authenticator { // eslint-disable-next-line sonarjs/no-small-switch switch (authMethod.toLowerCase()) { case 'basic': { - const credentials = Buffer.from(authString, 'base64').toString('utf-8'); - return this.isValidBasic(credentials, ctx); + return this.isValidBasic(authString, ctx); } default: @@ -164,7 +392,7 @@ class Authenticator { /** * Send a response requesting basic auth. - * @param {http.ServerResponse} res + * @param {http.ServerResponse} res response */ requestBasic(res) { res.setHeader(Enum.Header.WWWAuthenticate, `Basic realm="${this.basicRealm}", charset="UTF-8"`); @@ -172,40 +400,20 @@ class Authenticator { } - /** - * - * @param {String} cookieHeader - */ - static _cookieParse(cookieHeader) { - const cookie = {}; - (cookieHeader || '').split(/; */).forEach((field) => { - const [ name, value ] = common.splitFirst(field, '=', null).map((x) => x && decodeURIComponent(x.trim())); - if (name && !(name in cookie)) { - if (value && value.startsWith('"') && value.endsWith('"')) { - cookie[name] = value.slice(1, -1); // eslint-disable-line security/detect-object-injection - } else { - cookie[name] = value; // eslint-disable-line security/detect-object-injection - } - } - }); - return cookie; - } - - /** * Attempt to parse a session cookie, and determine if it contains an * authenticated user. - * Restores ctx.session from cookie data, sets ctx.authenticationId if identifier exists. - * @param {Object} ctx - * @param {String} cookieHeader - * @returns {Boolean} + * Restores ctx.session from cookie data, sets ctx.authenticationId to + * identifier or profile for session. + * @param {object} ctx context + * @param {object} ctx.cookie cookies object + * @returns {Promise} is valid */ - async isValidCookieAuth(ctx, cookieHeader) { + async isValidCookieAuth(ctx) { const _scope = _fileScope('isValidCookieAuth'); - this.logger.debug(_scope, 'called', { ctx, cookieHeader }); + this.logger.debug(_scope, 'called', { ctx }); - const cookie = Authenticator._cookieParse(cookieHeader); - const cookieValue = cookie[Enum.SessionCookie]; + const cookieValue = ctx.cookie?.[Enum.SessionCookie]; if (!cookieValue) { return false; @@ -215,7 +423,7 @@ class Authenticator { this.logger.debug(_scope, 'unpacked cookie', { ctx }); const hasIdentifier = !!ctx.session.authenticatedIdentifier; - const hasProfile = !!ctx.session.authenticatedProfile && this.authnEnabled.includes('indieAuth'); + const hasProfile = !!ctx.session.authenticatedProfile && this.authnEnabled.has('indieAuth'); const isValid = hasIdentifier || hasProfile; if (isValid) { ctx.authenticationId = ctx.session.authenticatedIdentifier || ctx.session.authenticatedProfile; @@ -223,7 +431,7 @@ class Authenticator { return isValid; } catch (e) { - this.logger.debug(_scope, 'could not unpack cookie', { error:e, ctx }); + this.logger.debug(_scope, 'could not unpack cookie', { error: e, ctx }); return false; } } @@ -231,16 +439,23 @@ class Authenticator { /** * Check for a valid session. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} loginPath - * @param {Boolean} required - * @param {Boolean} profilesAllowed - * @returns {Boolean} + * If a valid session cookie is present, refreshes the timeout for it. + * If not, and one is required, redirects to the login url. + * Convenience wrappers for option combinations: + * @see sessionRequired + * @see sessionRequiredLocal + * @see sessionOptional + * @see sessionOptionalLocal + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} loginPath url path to redirect to when login is required + * @param {boolean} required redirect to login url if no valid session + * @param {boolean} profilesAllowed if true, an indieauth session is valid, otherwise only identifier/credential + * @returns {Promise} is valid session */ async sessionCheck(req, res, ctx, loginPath, required = true, profilesAllowed = true) { - const _scope = _fileScope('check'); + const _scope = _fileScope('sessionCheck'); this.logger.debug(_scope, 'called', { ctx, loginPath, required, profilesAllowed }); if (this.secureAuthOnly @@ -253,27 +468,30 @@ class Authenticator { } } - const sessionCookie = req.getHeader(Enum.Header.Cookie); - if (sessionCookie - && await this.isValidCookieAuth(ctx, sessionCookie) + if (await this.isValidCookieAuth(ctx) && (ctx.session.authenticatedIdentifier || (profilesAllowed && ctx.session.authenticatedProfile))) { this.logger.debug(_scope, 'valid session cookie', { ctx }); + // Refresh timeout on valid session. + common.addCookie(res, Enum.SessionCookie, ctx.cookie[Enum.SessionCookie], { + httpOnly: true, + maxAge: this.cookieLifespan, + sameSite: 'Lax', + path: `${this.proxyPrefix}/`, + secure: this.secureAuthOnly, + }); return true; } if (required) { // Clear any existing invalid session - const cookieParts = [ - `${Enum.SessionCookie}=""`, - 'HttpOnly', - 'Max-Age=0', - `Path=${this.options.dingus.proxyPrefix}/`, - ]; - if (this.options.authenticator.secureAuthOnly) { - cookieParts.push('Secure'); - } - res.setHeader(Enum.Header.SetCookie, cookieParts.join('; ')); + common.addCookie(res, Enum.SessionCookie, '""', { + httpOnly: true, + maxAge: 0, + sameSite: 'Lax', + path: `${this.proxyPrefix}/`, + secure: this.secureAuthOnly, + }); res.statusCode = 302; res.setHeader(Enum.Header.Location, `${loginPath}?r=${encodeURIComponent(req.url)}`); @@ -286,11 +504,11 @@ class Authenticator { /** * Requires a valid session with a local identifier. Redirects to loginPath if not. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} loginPath - * @returns {Boolean} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} loginPath url path to redirect to when login is needed + * @returns {Promise} is valid session */ async sessionRequiredLocal(req, res, ctx, loginPath) { return this.sessionCheck(req, res, ctx, loginPath, true, false); @@ -299,11 +517,11 @@ class Authenticator { /** * Requires a valid session with either a local identifier or a profile. Redirects to loginPath if not. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} loginPath - * @returns {Boolean} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} loginPath url path to redirect to when login is needed + * @returns {Promise} is valid session */ async sessionRequired(req, res, ctx, loginPath) { return this.sessionCheck(req, res, ctx, loginPath); @@ -312,11 +530,10 @@ class Authenticator { /** * Check for a valid session with a local identifier, but do nothing if not. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} loginPath - * @returns {Boolean} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @returns {Promise} is valid session */ async sessionOptionalLocal(req, res, ctx) { return this.sessionCheck(req, res, ctx, undefined, false, false); @@ -325,11 +542,10 @@ class Authenticator { /** * Check for a valid session with either a local identifier or a profile, but do nothing if not. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} loginPath - * @returns {Boolean} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @returns {Promise} is valid session */ async sessionOptional(req, res, ctx) { return this.sessionCheck(req, res, ctx, undefined, false); @@ -338,37 +554,42 @@ class Authenticator { /** * Require auth for an API endpoint. - * Check for valid local identifier in Authorization header; optionally - * fall back to session cookie if no header provided. + * Check for valid local identifier in Authorization header; + * optionally fall back to session cookie if no header provided. * Prompts for Basic auth if not valid. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {Boolean} sessionAlsoValid + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {boolean} sessionAlsoValid fall back to session if no authz header + * @returns {Promise} is valid */ async apiRequiredLocal(req, res, ctx, sessionAlsoValid = true) { const _scope = _fileScope('apiRequiredLocal'); this.logger.debug(_scope, 'called', { ctx, sessionAlsoValid }); - // If a Authorization header was provided, never consider session as a fallback. - const authorizationHeader = req.getHeader(Enum.Header.Authorization); - if (authorizationHeader) { - if (await this.isValidAuthorization(authorizationHeader, ctx)) { - this.logger.debug(_scope, 'valid authorization', { ctx, sessionAlsoValid }); - return true; - } - } else { - if (sessionAlsoValid - && await this.sessionCheck(req, res, ctx, undefined, false, false)) { + try { + // If a Authorization header was provided, never consider session as a fallback. + const authorizationHeader = req.getHeader(Enum.Header.Authorization); + if (authorizationHeader) { + if (await this.isValidAuthorization(authorizationHeader, ctx)) { + this.logger.debug(_scope, 'valid authorization', { ctx, sessionAlsoValid }); + return true; + } + } else if (sessionAlsoValid + && await this.sessionCheck(req, res, ctx, undefined, false, false)) { this.logger.debug(_scope, 'valid session', { ctx, sessionAlsoValid }); return true; } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e }); + throw e; } this.logger.debug(_scope, 'invalid authorization', { ctx, sessionAlsoValid }); this.requestBasic(res); } + } -module.exports = Authenticator; \ No newline at end of file +module.exports = Authenticator;