bump package version to 1.5.0
[squeep-authentication-module] / lib / session-manager.js
index 2d3d96cd10706e07512ee3d908a6d849adb829b5..d399e5bd1fb480044459cde001e7eda8a90f213b 100644 (file)
@@ -8,27 +8,38 @@
 const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper');
 const { MysteryBox } = require('@squeep/mystery-box');
 const { TOTP } = require('@squeep/totp');
-const { randomUUID } = require('crypto');
+const { randomUUID } = require('node:crypto');
 const common = require('./common');
 const Enum = require('./enum');
 const Template = require('./template');
 
 const _fileScope = common.fileScope(__filename);
 
+/**
+ * @typedef {import('node:http')} http
+ * @typedef {import('./authenticator')} Authenticator
+ */
+/**
+ * @typedef {object} ConsoleLike
+ * @property {Function} debug log debug
+ * @property {Function} error log error
+ * @property {Function} info log info
+ */
+
 class SessionManager {
   /**
-   * @param {Console} logger
-   * @param {Authenticator} authenticator
-   * @param {Object} options
-   * @param {Object} options.authenticator
-   * @param {String[]} options.authenticator.authnEnabled
-   * @param {Number=} options.authenticator.inactiveSessionLifespanSeconds
-   * @param {Boolean} options.authenticator.secureAuthOnly
-   * @param {Object=} options.dingus
-   * @param {String=} options.dingus.proxyPrefix
-   * @param {String} options.dingus.selfBaseUrl
-   * @param {Object} options.manager
-   * @param {String} options.manager.pageTitle
+   * @param {ConsoleLike} logger logger
+   * @param {Authenticator} authenticator authenticator instance
+   * @param {object} options options
+   * @param {object} options.authenticator authenticator instance options
+   * @param {string[]} options.authenticator.authnEnabled authentication methods enabled
+   * @param {number=} options.authenticator.inactiveSessionLifespanSeconds session timeout
+   * @param {boolean} options.authenticator.secureAuthOnly allow only https
+   * @param {object=} options.dingus dingus options
+   * @param {string=} options.dingus.proxyPrefix prefix on route paths
+   * @param {string} options.dingus.selfBaseUrl base url
+   * @param {object} options.manager manager options
+   * @param {string} options.manager.pageTitle page title
    */
   constructor(logger, authenticator, options) {
     this.logger = logger;
@@ -46,10 +57,10 @@ class SessionManager {
 
   /**
    * Set or update our session cookie.
-   * @param {http.ServerResponse} res
-   * @param {Object=} session
-   * @param {Number=} maxAge
-   * @param {String=} path
+   * @param {http.ServerResponse} res respoonse
+   * @param {object=} session session object
+   * @param {number=} maxAge session validity in seconds
+   * @param {string=} path session cookie path
    */
   async _sessionCookieSet(res, session, maxAge = this.cookieLifespan, path = '/') {
     const cookieName = Enum.SessionCookie;
@@ -66,22 +77,22 @@ class SessionManager {
 
   /**
    * Remove any current session cookie.
-   * @param {http.ServerResponse} res
-   * @param {String} path
+   * @param {http.ServerResponse} res response
+   * @param {string} path session cookie path
    */
   async _sessionCookieClear(res, path = '/') {
     await this._sessionCookieSet(res, undefined, 0, path);
   }
 
   /**
-   * @typedef {(pagePathLevel: Number, ctx: Object, htmlOptions: Object) => void} AppTemplateCallback
+   * @typedef {(pagePathLevel: number, ctx: object, htmlOptions: object) => void} AppTemplateCallback
    */
 
   /**
    * GET request for establishing admin session.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {AppTemplateCallback} appCb
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
    */
   async getAdminLogin(res, ctx, appCb) {
     const _scope = _fileScope('getAdminLogin');
@@ -108,9 +119,9 @@ class SessionManager {
 
   /**
    * POST request for taking form data to establish admin session.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {AppTemplateCallback} appCb
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
    */
   async postAdminLogin(res, ctx, appCb) {
     const _scope = _fileScope('postAdminLogin');
@@ -136,7 +147,7 @@ class SessionManager {
     try {
       me = new URL(ctx.parsedBody['me']);
       meAutoScheme = !!ctx.parsedBody['me_auto_scheme'];
-    } catch (e) {
+    } catch (e) { // eslint-disable-line no-unused-vars
       this.logger.debug(_scope, 'failed to parse supplied profile url', { ctx });
       ctx.errors.push(`Unable to understand '${ctx.parsedBody['me']}' as a profile URL.`);
     }
@@ -161,7 +172,7 @@ class SessionManager {
       // fetch and parse me for 'authorization_endpoint' relation links
       try {
         authorizationEndpoint = new URL(profile.metadata.authorizationEndpoint);
-      } catch (e) {
+      } catch (e) { // eslint-disable-line no-unused-vars
         ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.metadata.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`);
       }
 
@@ -175,7 +186,7 @@ class SessionManager {
             this.logger.debug(_scope, 'supplied issuer url invalid', { ctx });
             ctx.errors.push('Authorization server provided an invalid issuer field.');
           }
-        } catch (e) {
+        } catch (e) { // eslint-disable-line no-unused-vars
           this.logger.debug(_scope, 'failed to parse supplied issuer url', { ctx });
           ctx.errors.push('Authorization server provided an unparsable issuer field.');
         }
@@ -227,15 +238,15 @@ class SessionManager {
 
 
   /**
-   * @typedef {Object} OTPState
-   * @property {String} authenticatedIdentifier
-   * @property {Buffer|String} key
-   * @property {Number} attempt
-   * @property {Number} epochMs
-   * @property {String} redirect
+   * @typedef {object} OTPState
+   * @property {string} authenticatedIdentifier identifier of logging-in user
+   * @property {Buffer | string} key otp key
+   * @property {number} attempt counter
+   * @property {number} epochMs started
+   * @property {string} redirect where to go after successful otp entry
    */
   /**
-   * @param {OTPState} otpState
+   * @param {OTPState} otpState otp state
    */
   static _validateOTPState(otpState) {
     if (!otpState.authenticatedIdentifier) {
@@ -260,12 +271,13 @@ class SessionManager {
    * Check if processing an OTP entry attempt.  If not, resume login flow.
    * If so, validate otp and establish session, else reprompt for OTP, or
    * return to login entry after too many failures.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {Object} ctx.parsedBody
-   * @param {String} ctx.parsedBody.state
-   * @param {String} ctx.parsedBody.otp
-   * @returns {Promise<Boolean>} true if otp was handled, otherwise false indicates further login processing needed
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {object} ctx.parsedBody submitted data
+   * @param {string} ctx.parsedBody.state packed state
+   * @param {string} ctx.parsedBody.otp entered code
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
+   * @returns {Promise<boolean>} true if otp was handled, otherwise false indicates further login processing needed
    */
   async _otpSubmission(res, ctx, appCb) {
     const _scope = _fileScope('_otpSubmission');
@@ -279,7 +291,7 @@ class SessionManager {
       // Ignore and continue back to main login.
       return false;
     }
-    /** @type OTPState */
+    /** @type {OTPState} */
     let state;
     try {
       state = await this.mysteryBox.unpack(stateBox);
@@ -337,9 +349,10 @@ class SessionManager {
 
   /**
    * 
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @returns {Promise<Boolean>} true if handled, false if flow should continue
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
+   * @returns {Promise<boolean>} true if handled, false if flow should continue
    */
   async _localUserAuth(res, ctx, appCb) {
     const _scope = _fileScope('_localUserAuth');
@@ -394,8 +407,8 @@ class SessionManager {
 
   /**
    * GET request to remove current credentials.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
    */
   async getAdminLogout(res, ctx) {
     const _scope = _fileScope('getAdminLogout');
@@ -416,9 +429,9 @@ class SessionManager {
   /**
    * GET request for returning IndieAuth redirect.
    * This currently only redeems a scope-less profile.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {AppTemplateCallback} appCb
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
    */
   async getAdminIA(res, ctx, appCb) {
     const _scope = _fileScope('getAdminIA');
@@ -438,7 +451,7 @@ class SessionManager {
         ctx.session = await this.mysteryBox.unpack(cookieValue);
         this.logger.debug(_scope, 'restored session from cookie', { ctx });
       } catch (e) {
-        this.logger.debug(_scope, 'could not unpack cookie');
+        this.logger.debug(_scope, 'could not unpack cookie', { error: e });
         ctx.errors.push('invalid cookie');
       }
     }
@@ -481,7 +494,7 @@ class SessionManager {
     let redeemProfileUrl;
     try {
       redeemProfileUrl = new URL(ctx.session.authorizationEndpoint);
-    } catch (e) {
+    } catch (e) { // eslint-disable-line no-unused-vars
       this.logger.debug(_scope, 'failed to parse restored session authorization endpoint as url', { ctx });
       ctx.errors.push('invalid cookie');
     }
@@ -531,11 +544,17 @@ class SessionManager {
   }
 
 
+  /**
+   * @typedef {object} AuthInfo
+   * @property {string} identifier identifier
+   * @property {string} credential hashed credential
+   * @property {string=} otpKey otp key
+   */
   /**
    * Page for modifying credentials and OTP.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {AppTemplateCallback} appCb
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
    */
   async getAdminSettings(res, ctx, appCb) {
     const _scope = _fileScope('getAdminSettings');
@@ -562,10 +581,9 @@ class SessionManager {
 
   /**
    * Page for modifying credentials and OTP.
-   * @param {http.ServerResponse} res
-   * @param {Object} ctx
-   * @param {Object[]=} appNavLinks
-   * @param {AppTemplateCallback} appCb
+   * @param {http.ServerResponse} res response
+   * @param {object} ctx context
+   * @param {AppTemplateCallback} appCb function to mogrify template htmlOptions
    */
   async postAdminSettings(res, ctx, appCb) {
     const _scope = _fileScope('postAdminSettings');
@@ -586,7 +604,7 @@ class SessionManager {
             await this._otpDisable(dbCtx, ctx, authData);
             return;
 
-          case 'confirm': 
+          case 'confirm':
             await this._otpConfirm(dbCtx, ctx);
             return;
 
@@ -614,9 +632,9 @@ class SessionManager {
 
   /**
    * Submission to disable OTP.
-   * @param {*} dbCtx 
-   * @param {*} ctx 
-   * @param {AuthInfo} authData 
+   * @param {*} dbCtx db context
+   * @param {*} ctx context
+   * @param {AuthInfo} authData auth info
    */
   async _otpDisable(dbCtx, ctx, authData) {
     const _scope = _fileScope('_otpDisable');
@@ -635,7 +653,7 @@ class SessionManager {
 
   /**
    * Submission to enable OTP.
-   * @param {Object} ctx 
+   * @param {object} ctx context
    */
   async _otpEnable(ctx) {
     const _scope = _fileScope('_otpEnable');
@@ -657,8 +675,8 @@ class SessionManager {
 
   /**
    * Submission to confirm enabling OTP.
-   * @param {*} dbCtx 
-   * @param {Object} ctx 
+   * @param {*} dbCtx  db context
+   * @param {object} ctx context
    */
   async _otpConfirm(dbCtx, ctx) {
     const _scope = _fileScope('_otpConfirm');
@@ -715,9 +733,9 @@ class SessionManager {
 
   /**
    * Submission to set new credential.
-   * @param {*} dbCtx 
-   * @param {Object} ctx 
-   * @param {AuthInfo} authData 
+   * @param {*} dbCtx db context
+   * @param {object} ctx context
+   * @param {AuthInfo} authData auth info
    */
   async _credentialUpdate(dbCtx, ctx, authData) {
     const _scope = _fileScope('_credentialUpdate');