update devDependencies, clean up lint issues
[squeep-authentication-module] / lib / authenticator.js
index 7390db5d8803c8271e0cbc32c254e3198d057df0..2534cfa2d5224fe9bdf4c1c407472deb3cd8bd1f 100644 (file)
@@ -16,43 +16,54 @@ const _fileScope = common.fileScope(__filename);
  * Interacts with the authentication database interface.
  */
 
  * Interacts with the authentication database interface.
  */
 
+/**
+ * @typedef {import('node:http')} http
+ */
+
 class Authenticator {
   /**
 class Authenticator {
   /**
-   * @typedef {Object} AuthInfo
-   * @property {String} identifier
-   * @property {String} credential
-   * @property {String=} otpKey
+   * @typedef {object} AuthInfo
+   * @property {string} identifier identifier
+   * @property {string} credential hashed credential
+   * @property {string=} otpKey optional otp key
    */
   /**
    * @callback DBContextExec
    */
   /**
    * @callback DBContextExec
-   * @param {Object} dbCtx
+   * @param {object} dbCtx
    * @returns {Promise<any>}
    */
   /**
    * @returns {Promise<any>}
    */
   /**
-   * @typedef {Object} AuthDBInterface
-   * @property {(DBContextExec) => Promise<any>} context
-   * @property {(dbCtx: any, identifier: String) => Promise<AuthInfo> } authenticationGet
-   * @property {(dbCtx: any, identifier: String) => Promise<void>} authenticationSuccess
-   * @property {(dbCtx: any, identifier: String, credential: String, otpKey: String=) => Promise<void>} authenticationInsertIdentifier
-   * @property {(dbCtx: any, identifier: String, otpKey: String) => Promise<void>} authenticationUpdateOTPKey
-   * @property {(dbCtx: any, identifier: String, credential: AuthInfo) => Promise<void>} authenticationUpdateCredential
+   * @typedef {object} AuthDBInterface
+   * @property {(DBContextExec) => Promise<any>} context db context runner
+   * @property {(dbCtx: any, identifier: string) => Promise<AuthInfo>} authenticationGet get an authentication entry
+   * @property {(dbCtx: any, identifier: string) => Promise<void>} authenticationSuccess store a successful authentication event
+   * @property {(dbCtx: any, identifier: string, credential: string, otpKey: string=) => Promise<void>} authenticationUpsert update an authentication entry
+   * @property {(dbCtx: any, identifier: string, otpKey: string) => Promise<void>} authenticationUpdateOTPKey update otp key
+   * @property {(dbCtx: any, identifier: string, credential: AuthInfo) => Promise<void>} authenticationUpdateCredential update credential
    */
   /**
    */
   /**
-   * @param {Console} logger
-   * @param {AuthDBInterface} db
-   * @param {Object} options
-   * @param {String|String[]} options.encryptionSecret
-   * @param {Object} options.authenticator
-   * @param {Boolean} options.authenticator.secureAuthOnly
-   * @param {String[]} options.authenticator.forbiddenPAMIdentifiers
-   * @param {String[]} options.authenticator.authnEnabled in order of preference for storing new credentials
-   * @param {Number=} options.authenticator.inactiveSessionLifespanSeconds
-   * @param {String[]=} options.authenticator.loginBlurb
-   * @param {String[]=} options.authenticator.indieAuthBlurb
-   * @param {String[]=} options.authenticator.userBlurb
-   * @param {String[]=} options.authenticator.otpBlurb
-   * @param {String=} options.dingus
-   * @param {String=} options.dingus.proxyPrefix
+   * @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.sessionCookieSameSite sameSite setting for session cookie, default Lax
+   * @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 {object=} options.dingus dingus options
+   * @param {string=} options.dingus.proxyPrefix base url prefix
    */
   constructor(logger, db, options) {
     this.logger = logger;
    */
   constructor(logger, db, options) {
     this.logger = logger;
@@ -60,8 +71,13 @@ class Authenticator {
     this.options = options;
     this.basicRealm = options.authenticator.basicRealm || packageName;
     this.secureAuthOnly = options.authenticator.secureAuthOnly ?? true;
     this.options = options;
     this.basicRealm = options.authenticator.basicRealm || packageName;
     this.secureAuthOnly = options.authenticator.secureAuthOnly ?? true;
+    this.sameSite = options.authenticator.sessionCookieSameSite || 'Lax';
     this.proxyPrefix = options.dingus?.proxyPrefix ?? '';
 
     this.proxyPrefix = options.dingus?.proxyPrefix ?? '';
 
+    if (!['None', 'Lax', 'Strict'].includes(this.sameSite)) {
+      throw new RangeError(`invalid sameSite value "${this.sameSite}"`);
+    }
+
     // First construct map of all available code-supported auth mechanisms.
     this.authn = {
       indieAuth: {},
     // First construct map of all available code-supported auth mechanisms.
     this.authn = {
       indieAuth: {},
@@ -69,11 +85,11 @@ class Authenticator {
     };
     try {
       this.authn.argon2 = require('argon2');
     };
     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 ?? ['root'];
     try {
       this.authn.pam = require('node-linux-pam');
       this.forbiddenPAMIdentifiers = options.authenticator.forbiddenPAMIdentifiers ?? ['root'];
-    } catch (e) { /**/ }
+    } catch (e) { /**/ } // eslint-disable-line no-unused-vars
 
     // Track which authn methods we can change credentials et cetera.
     const authnUpdatable = ['plain', 'argon2'];
 
     // Track which authn methods we can change credentials et cetera.
     const authnUpdatable = ['plain', 'argon2'];
@@ -107,17 +123,17 @@ class Authenticator {
   /**
    * Populate the authentication database with a new identifier, the
    * secured credential, and optionally an OTP key.
   /**
    * Populate the authentication database with a new identifier, the
    * secured credential, and optionally an OTP key.
-   * @param {*} dbCtx
-   * @param {String} identifier
-   * @param {String} credential plaintext
-   * @param {String=} otpKey
+   * @param {*} dbCtx db context
+   * @param {string} identifier identifier
+   * @param {string} credential plaintext
+   * @param {string=} otpKey otp key
    * @returns {Promise<void>}
    */
   async createIdentifier(dbCtx, identifier, credential, otpKey = null) {
     const _scope = _fileScope('createIdentifier');
     try {
       const secureCredential = await this._secureCredential(credential);
    * @returns {Promise<void>}
    */
   async createIdentifier(dbCtx, identifier, credential, otpKey = null) {
     const _scope = _fileScope('createIdentifier');
     try {
       const secureCredential = await this._secureCredential(credential);
-      await this.db.authenticationInsertIdentifier(dbCtx, identifier, secureCredential, otpKey);
+      await this.db.authenticationUpsert(dbCtx, identifier, secureCredential, otpKey);
     } catch (e) {
       this.logger.error(_scope, 'failed', { error: e, identifier });
       throw e;
     } catch (e) {
       this.logger.error(_scope, 'failed', { error: e, identifier });
       throw e;
@@ -128,9 +144,9 @@ class Authenticator {
   /**
    * Update the authentication database with a new secured credential
    * for an indentifier.
   /**
    * Update the authentication database with a new secured credential
    * for an indentifier.
-   * @param {*} dbCtx
-   * @param {*} identifier
-   * @param {*} credential plaintext
+   * @param {*} dbCtx dbCtx
+   * @param {string} identifier identifier
+   * @param {string} credential plaintext
    * @returns {Promise<void>}
    */
   async updateCredential(dbCtx, identifier, credential) {
    * @returns {Promise<void>}
    */
   async updateCredential(dbCtx, identifier, credential) {
@@ -148,9 +164,9 @@ class Authenticator {
 
   /**
    * Encode a plaintext credential in the preferred way to store in database.
 
   /**
    * Encode a plaintext credential in the preferred way to store in database.
-   * @param {String} credential
-   * @param {String=} authn
-   * @returns {Promise<String>}
+   * @param {string} credential plaintext
+   * @param {string=} authn authentication mechanism
+   * @returns {Promise<string>} encoded credential
    */
   async _secureCredential(credential, authn = this.authnPreferred) {
     const _scope = _fileScope('_secureCredential');
    */
   async _secureCredential(credential, authn = this.authnPreferred) {
     const _scope = _fileScope('_secureCredential');
@@ -175,8 +191,8 @@ class Authenticator {
   /**
    * Checks a supplied credential against supplied data.
    * @param {AuthInfo} authData from database
   /**
    * Checks a supplied credential against supplied data.
    * @param {AuthInfo} authData from database
-   * @param {String} credential plaintext
-   * @returns {Promise<Boolean>}
+   * @param {string} credential plaintext
+   * @returns {Promise<boolean>} is valid
    */
   async _validateAuthDataCredential(authData, credential) {
     const _scope = _fileScope('_validateAuthDataCredential');
    */
   async _validateAuthDataCredential(authData, credential) {
     const _scope = _fileScope('_validateAuthDataCredential');
@@ -204,9 +220,9 @@ class Authenticator {
 
   /**
    * Check argon2.
 
   /**
    * Check argon2.
-   * @param {AuthInfo} authData
-   * @param {String} credential
-   * @returns {Promise<Boolean>}
+   * @param {AuthInfo} authData auth entry
+   * @param {string} credential to check
+   * @returns {Promise<boolean>} is valid
    */
   async _isValidArgon2Identifier(authData, credential) {
     return await this.authn.argon2.verify(authData.credential, credential);
    */
   async _isValidArgon2Identifier(authData, credential) {
     return await this.authn.argon2.verify(authData.credential, credential);
@@ -215,9 +231,9 @@ class Authenticator {
 
   /**
    * Check plaintext.
 
   /**
    * Check plaintext.
-   * @param {AuthInfo} authData
-   * @param {String} credential
-   * @returns {Promise<Boolean>}
+   * @param {AuthInfo} authData auth entry
+   * @param {string} credential to check
+   * @returns {Promise<boolean>} is valid
    */
   static _isValidPlainIdentifier(authData, credential) {
     return authData.credential.substring('$plain$'.length) === credential;
    */
   static _isValidPlainIdentifier(authData, credential) {
     return authData.credential.substring('$plain$'.length) === credential;
@@ -226,9 +242,9 @@ class Authenticator {
 
   /**
    * Check system PAM.
 
   /**
    * Check system PAM.
-   * @param {AuthInfo} authData
-   * @param {String} credential
-   * @returns {Promise<Boolean>}
+   * @param {AuthInfo} authData auth entry
+   * @param {string} credential to check
+   * @returns {Promise<boolean>} is valid
    */
   async _isValidPAMIdentifier(authData, credential) {
     const _scope = _fileScope('_isValidPAMIdentifier');
    */
   async _isValidPAMIdentifier(authData, credential) {
     const _scope = _fileScope('_isValidPAMIdentifier');
@@ -254,10 +270,10 @@ class Authenticator {
    * Check local auth entries.
    * Sets ctx.authenticatedId if valid.
    * Sets ctx.otpKey if account has otpKey.
    * Check local auth entries.
    * Sets ctx.authenticatedId if valid.
    * Sets ctx.otpKey if account has otpKey.
-   * @param {String} identifier
-   * @param {String} credential
-   * @param {Object} ctx
-   * @returns {Promise<Boolean>}
+   * @param {string} identifier identifier
+   * @param {string} credential to check
+   * @param {object} ctx context
+   * @returns {Promise<boolean>} is valid
    */
   async isValidIdentifierCredential(identifier, credential, ctx) {
     const _scope = _fileScope('isValidIdentifierCredential');
    */
   async isValidIdentifierCredential(identifier, credential, ctx) {
     const _scope = _fileScope('isValidIdentifierCredential');
@@ -292,13 +308,16 @@ class Authenticator {
 
 
   /**
 
 
   /**
-   * 
-   * @param {OTPState} state
-   * @param {String} state.key
-   * @param {Number} state.attempt
-   * @param {Number} state.epochMs
-   * @param {String} otp
-   * @returns {String} Enum.OTPResult
+   * @typedef {object} OTPState
+   * @property {string} key otp key
+   * @property {number} attempt count of attempts
+   * @property {number} epochMs when entry was initiated
+   */
+  /**
+   * 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({
    */
   checkOTP(state, otp) {
     const totp = new this.TOTP({
@@ -319,16 +338,34 @@ class Authenticator {
   }
 
 
   }
 
 
+  /**
+   * 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.db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey);
+      this.logger.info(_scope, 'otp key updated');
+    } catch (e) {
+      this.logger.error(_scope, 'failed', { error: e, identifier });
+    }
+  }
+
+
   /**
    * Check for valid Basic auth, updates ctx with identifier if valid.
   /**
    * Check for valid Basic auth, updates ctx with identifier if valid.
-   * @param {String} credentials
-   * @param {Object} ctx
-   * @returns {Promise<Boolean>}
+   * @param {string} authValue basic auth value (base64)
+   * @param {object} ctx context
+   * @returns {Promise<boolean>} is valid
    */
    */
-  async isValidBasic(credentials, ctx) {
+  async isValidBasic(authValue, ctx) {
     const _scope = _fileScope('isValidBasic');
     this.logger.debug(_scope, 'called', { 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);
     const [identifier, credential] = common.splitFirst(credentials, ':', '');
 
     return this.isValidIdentifierCredential(identifier, credential, ctx);
@@ -337,9 +374,9 @@ class Authenticator {
 
   /**
    * Determine which Authorization header is available, and if it is valid.
 
   /**
    * Determine which Authorization header is available, and if it is valid.
-   * @param {String} authorizationHeader
-   * @param {Object} ctx
-   * @returns {Promise<Boolean>}
+   * @param {string} authorizationHeader request header
+   * @param {object} ctx context
+   * @returns {Promise<boolean>} is valid
    */
   async isValidAuthorization(authorizationHeader, ctx) {
     const _scope = _fileScope('isValidAuthorization');
    */
   async isValidAuthorization(authorizationHeader, ctx) {
     const _scope = _fileScope('isValidAuthorization');
@@ -349,8 +386,7 @@ class Authenticator {
     // eslint-disable-next-line sonarjs/no-small-switch
     switch (authMethod.toLowerCase()) {
       case 'basic': {
     // 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:
       }
 
       default:
@@ -362,7 +398,7 @@ class Authenticator {
 
   /**
    * Send a response requesting basic auth.
 
   /**
    * 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"`);
    */
   requestBasic(res) {
     res.setHeader(Enum.Header.WWWAuthenticate, `Basic realm="${this.basicRealm}", charset="UTF-8"`);
@@ -375,9 +411,9 @@ class Authenticator {
    * authenticated user.
    * Restores ctx.session from cookie data, sets ctx.authenticationId to
    * identifier or profile for session.
    * authenticated user.
    * Restores ctx.session from cookie data, sets ctx.authenticationId to
    * identifier or profile for session.
-   * @param {Object} ctx
-   * @param {Object} ctx.cookie
-   * @returns {Promise<Boolean>}
+   * @param {object} ctx context
+   * @param {object} ctx.cookie cookies object
+   * @returns {Promise<boolean>} is valid
    */
   async isValidCookieAuth(ctx) {
     const _scope = _fileScope('isValidCookieAuth');
    */
   async isValidCookieAuth(ctx) {
     const _scope = _fileScope('isValidCookieAuth');
@@ -416,13 +452,13 @@ class Authenticator {
    * @see sessionRequiredLocal
    * @see sessionOptional
    * @see sessionOptionalLocal
    * @see sessionRequiredLocal
    * @see sessionOptional
    * @see sessionOptionalLocal
-   * @param {http.ClientRequest} req
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {String} loginPath
-   * @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<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 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<boolean>} is valid session
    */
   async sessionCheck(req, res, ctx, loginPath, required = true, profilesAllowed = true) {
     const _scope = _fileScope('sessionCheck');
    */
   async sessionCheck(req, res, ctx, loginPath, required = true, profilesAllowed = true) {
     const _scope = _fileScope('sessionCheck');
@@ -446,10 +482,11 @@ class Authenticator {
       common.addCookie(res, Enum.SessionCookie, ctx.cookie[Enum.SessionCookie], {
         httpOnly: true,
         maxAge: this.cookieLifespan,
       common.addCookie(res, Enum.SessionCookie, ctx.cookie[Enum.SessionCookie], {
         httpOnly: true,
         maxAge: this.cookieLifespan,
-        sameSite: 'Lax',
+        sameSite: this.sameSite,
         path: `${this.proxyPrefix}/`,
         secure: this.secureAuthOnly,
       });
         path: `${this.proxyPrefix}/`,
         secure: this.secureAuthOnly,
       });
+      res.setHeader(Enum.Header.SetLogin, Enum.LoginState.LoggedIn);
       return true;
     }
 
       return true;
     }
 
@@ -458,10 +495,11 @@ class Authenticator {
       common.addCookie(res, Enum.SessionCookie, '""', {
         httpOnly: true,
         maxAge: 0,
       common.addCookie(res, Enum.SessionCookie, '""', {
         httpOnly: true,
         maxAge: 0,
-        sameSite: 'Lax',
+        sameSite: this.sameSite,
         path: `${this.proxyPrefix}/`,
         secure: this.secureAuthOnly,
       });
         path: `${this.proxyPrefix}/`,
         secure: this.secureAuthOnly,
       });
+      res.setHeader(Enum.Header.SetLogin, Enum.LoginState.LoggedOut);
 
       res.statusCode = 302;
       res.setHeader(Enum.Header.Location, `${loginPath}?r=${encodeURIComponent(req.url)}`);
 
       res.statusCode = 302;
       res.setHeader(Enum.Header.Location, `${loginPath}?r=${encodeURIComponent(req.url)}`);
@@ -474,11 +512,11 @@ class Authenticator {
 
   /**
    * Requires a valid session with a local identifier. Redirects to loginPath if not.
 
   /**
    * 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 {Promise<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<boolean>} is valid session
    */
   async sessionRequiredLocal(req, res, ctx, loginPath) {
     return this.sessionCheck(req, res, ctx, loginPath, true, false);
    */
   async sessionRequiredLocal(req, res, ctx, loginPath) {
     return this.sessionCheck(req, res, ctx, loginPath, true, false);
@@ -487,11 +525,11 @@ class Authenticator {
 
   /**
    * Requires a valid session with either a local identifier or a profile. Redirects to loginPath if not.
 
   /**
    * 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 {Promise<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<boolean>} is valid session
    */
   async sessionRequired(req, res, ctx, loginPath) {
     return this.sessionCheck(req, res, ctx, loginPath);
    */
   async sessionRequired(req, res, ctx, loginPath) {
     return this.sessionCheck(req, res, ctx, loginPath);
@@ -500,11 +538,10 @@ class Authenticator {
 
   /**
    * Check for a valid session with a local identifier, but do nothing if not.
 
   /**
    * 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 {Promise<Boolean>}
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {Promise<boolean>} is valid session
    */
   async sessionOptionalLocal(req, res, ctx) {
     return this.sessionCheck(req, res, ctx, undefined, false, false);
    */
   async sessionOptionalLocal(req, res, ctx) {
     return this.sessionCheck(req, res, ctx, undefined, false, false);
@@ -513,11 +550,10 @@ class Authenticator {
 
   /**
    * Check for a valid session with either a local identifier or a profile, but do nothing if not.
 
   /**
    * 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 {Promise<Boolean>}
+   * @param {http.ClientRequest} req request
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @returns {Promise<boolean>} is valid session
    */
   async sessionOptional(req, res, ctx) {
     return this.sessionCheck(req, res, ctx, undefined, false);
    */
   async sessionOptional(req, res, ctx) {
     return this.sessionCheck(req, res, ctx, undefined, false);
@@ -529,27 +565,32 @@ class Authenticator {
    * 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.
    * 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
-   * @returns {Promise<Boolean}
+   * @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<boolean>} is valid
    */
   async apiRequiredLocal(req, res, ctx, sessionAlsoValid = true) {
     const _scope = _fileScope('apiRequiredLocal');
     this.logger.debug(_scope, 'called', { ctx, sessionAlsoValid });
 
    */
   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 });
+    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;
       }
         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.logger.debug(_scope, 'invalid authorization', { ctx, sessionAlsoValid });