update devDependencies, clean up lint issues
[squeep-authentication-module] / lib / authenticator.js
index eab1a1a2183ca67b660b826aae5481e76ffd8c98..2534cfa2d5224fe9bdf4c1c407472deb3cd8bd1f 100644 (file)
@@ -53,15 +53,16 @@ class Authenticator {
    * @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 {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 {string=} options.dingus dingus options
+   * @param {object=} options.dingus dingus options
    * @param {string=} options.dingus.proxyPrefix base url prefix
    */
   constructor(logger, db, options) {
@@ -70,8 +71,13 @@ class Authenticator {
     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 ?? '';
 
+    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: {},
@@ -351,14 +357,15 @@ class Authenticator {
 
   /**
    * Check for valid Basic auth, updates ctx with identifier if valid.
-   * @param {string} credentials basic auth field (decoded)
+   * @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 credentials = Buffer.from(authValue, 'base64').toString('utf-8');
     const [identifier, credential] = common.splitFirst(credentials, ':', '');
 
     return this.isValidIdentifierCredential(identifier, credential, ctx);
@@ -379,8 +386,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'); // FIXME: move into isValidBasic, why is it here?
-        return this.isValidBasic(credentials, ctx);
+        return this.isValidBasic(authString, ctx);
       }
 
       default:
@@ -476,10 +482,11 @@ class Authenticator {
       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,
       });
+      res.setHeader(Enum.Header.SetLogin, Enum.LoginState.LoggedIn);
       return true;
     }
 
@@ -488,10 +495,11 @@ class Authenticator {
       common.addCookie(res, Enum.SessionCookie, '""', {
         httpOnly: true,
         maxAge: 0,
-        sameSite: 'Lax',
+        sameSite: this.sameSite,
         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)}`);
@@ -567,17 +575,22 @@ class Authenticator {
     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;
       }
-    } 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 });