allow config of session cookie sameSite value
authorJustin Wind <justin.wind+git@gmail.com>
Sat, 18 May 2024 17:44:54 +0000 (10:44 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sat, 18 May 2024 17:44:54 +0000 (10:44 -0700)
lib/authenticator.js
lib/session-manager.js
test/lib/authenticator.js
test/lib/session-manager.js

index 3df0b9db17ccdf763ce13b6340e912b0d6545266..86b95d6d035e92284756476a1baae491908a5f18 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: {},
@@ -476,7 +482,7 @@ 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,
       });
@@ -488,7 +494,7 @@ class Authenticator {
       common.addCookie(res, Enum.SessionCookie, '""', {
         httpOnly: true,
         maxAge: 0,
-        sameSite: 'Lax',
+        sameSite: this.sameSite,
         path: `${this.proxyPrefix}/`,
         secure: this.secureAuthOnly,
       });
index d399e5bd1fb480044459cde001e7eda8a90f213b..0b4ce4eaff29db733a04c9c8776716e5290c6317 100644 (file)
@@ -35,6 +35,7 @@ class SessionManager {
    * @param {string[]} options.authenticator.authnEnabled authentication methods enabled
    * @param {number=} options.authenticator.inactiveSessionLifespanSeconds session timeout
    * @param {boolean} options.authenticator.secureAuthOnly allow only https
+   * @param {string=} options.authenticator.sessionCookieSameSite sameSite setting for session cookie, default Lax
    * @param {object=} options.dingus dingus options
    * @param {string=} options.dingus.proxyPrefix prefix on route paths
    * @param {string} options.dingus.selfBaseUrl base url
@@ -47,6 +48,8 @@ class SessionManager {
     this.db = authenticator.db; // TODO: take db arg in next major version bump
     this.options = options;
     this.proxyPrefix = options.dingus?.proxyPrefix ?? '';
+    this.secureAuthOnly = options.authenticator.secureAuthOnly ?? true;
+    this.sameSite = options.authenticator.sessionCookieSameSite || 'Lax';
     this.indieAuthCommunication = new IndieAuthCommunication(logger, options);
     this.mysteryBox = new MysteryBox(options);
     this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name)));
@@ -67,8 +70,8 @@ class SessionManager {
     const secureSession = session && await this.mysteryBox.pack(session) || '""';
     common.addCookie(res, cookieName, secureSession, {
       httpOnly: true,
-      sameSite: 'Lax',
-      secure: this.options.authenticator.secureAuthOnly,
+      sameSite: this.sameSite,
+      secure: this.secureAuthOnly,
       maxAge: session && maxAge || 0,
       path,
     });
index 75ae9a883547ba055f341a480104959d7fa50b7d..4768e0d5f94f226c5289361893c96ec9c79f134a 100644 (file)
@@ -53,6 +53,11 @@ describe('Authenticator', function () {
     authenticator = new Authenticator(stubLogger, stubDb, options);
   });
 
+  it('covers invalid sameSite', function () {
+    options.authenticator.sessionCookieSameSite = 'Sometimes';
+    assert.throws(() => new Authenticator(stubLogger, stubDb, options), RangeError);
+  });
+
   describe('createIdentifier', function () {
     let dbCtx;
     beforeEach(function () {
index 16243d732daf04ae6ea7400f137a35752e0af044..40322c19655b19ae433ef3d8d54662724f2b5d80 100644 (file)
@@ -50,6 +50,8 @@ describe('SessionManager', function () {
   describe('constructor', function () {
     it('covers options', function () {
       delete options.dingus.proxyPrefix;
+      delete options.authenticator.secureAuthOnly;
+      options.authenticator.sessionCookieSameSite = 'None';
       manager = new SessionManager(stubLogger, stubAuthenticator, options);
     });
   }); // constructor