updates to support IndieAuth spec 20220212 metadata and issuer
[squeep-authentication-module] / lib / session-manager.js
index f7cd33a1dcf31ac13104255610b2a0b01244b3b2..a97a0e0fd3f0edf2ed897e89c9f565e41843525c 100644 (file)
@@ -19,7 +19,8 @@ class SessionManager {
    * @param {Object} options
    * @param {Object} options.authenticator
    * @param {String[]} options.authenticator.authnEnabled
-   * @param {Object} options.authenticator.secureAuthOnly
+   * @param {Number=} options.authenticator.inactiveSessionLifespanSeconds
+   * @param {Boolean} options.authenticator.secureAuthOnly
    * @param {Object} options.dingus
    * @param {Object} options.dingus.proxyPrefix
    * @param {Object} options.dingus.selfBaseUrl
@@ -31,7 +32,7 @@ class SessionManager {
     this.indieAuthCommunication = new IndieAuthCommunication(logger, options);
     this.mysteryBox = new MysteryBox(logger, options);
 
-    this.cookieLifespan = 60 * 60 * 24 * 32;
+    this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32;
   }
 
 
@@ -121,6 +122,7 @@ class SessionManager {
       return;
     }
 
+    // Otherwise, carry on with IndieAuth handshake.
     let me, session, authorizationEndpoint;
     try {
       me = new URL(ctx.parsedBody['me']);
@@ -132,28 +134,51 @@ class SessionManager {
     if (this.options.authenticator.authnEnabled.includes('indieAuth')
     &&  me) {
       const profile = await this.indieAuthCommunication.fetchProfile(me);
-      if (!profile || !profile.authorizationEndpoint) {
+      if (!profile || !profile.metadata) {
         this.logger.debug(_scope, 'failed to find any profile information at url', { ctx });
         ctx.errors.push(`No profile information was found at '${me}'.`);
       } else {
         // fetch and parse me for 'authorization_endpoint' relation links
         try {
-          authorizationEndpoint = new URL(profile.authorizationEndpoint);
+          authorizationEndpoint = new URL(profile.metadata.authorizationEndpoint);
         } catch (e) {
-          ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`);
+          ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.metadata.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`);
+        }
+
+        if (profile.metadata.issuer) {
+          // Validate issuer
+          try {
+            const issuer = new URL(profile.metadata.issuer);
+            if (issuer.hash
+            ||  issuer.search
+            ||  issuer.protocol.toLowerCase() !== 'https:') { // stupid URL trailing colon thing
+              this.logger.debug(_scope, 'supplied issuer url invalid', { ctx });
+              ctx.errors.push('Authorization server provided an invalid issuer field.');
+            }
+          } catch (e) {
+            this.logger.debug(_scope, 'failed to parse supplied issuer url', { ctx });
+            ctx.errors.push('Authorization server provided an unparsable issuer field.');
+          }
+        } else {
+          this.logger.debug(_scope, 'no issuer in metadata, assuming legacy mode', { ctx });
+          // Strict 20220212 compliance would error here.
+          // ctx.errors.push('Authorization server did not provide issuer field, as required by current specification.');
         }
       }
 
       if (authorizationEndpoint) {
         const pkce = await IndieAuthCommunication.generatePKCE();
+
         session = {
           authorizationEndpoint: authorizationEndpoint.href,
           state: ctx.requestId,
           codeVerifier: pkce.codeVerifier,
           me,
           redirect,
+          issuer: profile.metadata.issuer,
         };
 
+        // Update auth endpoint parameters
         Object.entries({
           'response_type': 'code',
           'client_id': this.options.dingus.selfBaseUrl,
@@ -203,6 +228,7 @@ class SessionManager {
 
   /**
    * GET request for returning IndieAuth redirect.
+   * This currently only redeems a scope-less profile.
    * @param {http.ServerResponse} res
    * @param {Object} ctx
    */
@@ -230,6 +256,7 @@ class SessionManager {
     }
 
     // Validate unpacked session values
+    // ...
 
     // Add any auth errors
     if (ctx.queryParams['error']) {
@@ -251,6 +278,18 @@ class SessionManager {
       ctx.errors.push('invalid code');
     }
 
+    // check issuer
+    if (ctx.session.issuer) {
+      if (ctx.queryParams['iss'] !== ctx.session.issuer) {
+        this.logger.debug(_scope, 'issuer mismatch', { ctx });
+        ctx.errors.push('invalid issuer');
+      }
+    } else {
+      this.logger.debug(_scope, 'no issuer in metadata, assuming legacy mode', { ctx });
+      // Strict 20220212 compliance would error here. (Also earlier.)
+      // ctx.errors.push('invalid issuer');
+    }
+
     let redeemProfileUrl;
     try {
       redeemProfileUrl = new URL(ctx.session.authorizationEndpoint);
@@ -272,7 +311,7 @@ class SessionManager {
         const newProfileUrl = new URL(profile.me);
         // Rediscover auth endpoint for the new returned profile.
         const newProfile = await this.indieAuthCommunication.fetchProfile(newProfileUrl);
-        if (newProfile.authorizationEndpoint !== ctx.session.authorizationEndpoint) {
+        if (newProfile.metadata.authorizationEndpoint !== ctx.session.authorizationEndpoint) {
           this.logger.debug(_scope, 'mis-matched auth endpoints between provided me and canonical me', { ctx, profile, newProfile });
           ctx.errors.push('canonical profile url provided by authorization endpoint is not handled by that endpoint, cannot continue');
         } else {