default indieauth profile entry to https if scheme not specified
authorJustin Wind <justin.wind+git@gmail.com>
Thu, 23 Feb 2023 21:49:19 +0000 (13:49 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Thu, 23 Feb 2023 21:49:19 +0000 (13:49 -0800)
lib/authenticator.js
lib/session-manager.js
lib/template/login-html.js
test/lib/session-manager.js

index c69ada91c6ed6a5234942b1e6ae8eb92a420d7e6..5a4cd7d5f8beffad124ffb0bfcd5ed643c156c73 100644 (file)
@@ -86,7 +86,7 @@ class Authenticator {
         &&         this.authnEnabled.includes('pam')) {
           isValid = this._isValidPAMIdentifier(identifier, credential);
         } else {
-          this.logger.error(_scope, 'failed, unknown type of stored credential', { identifier, ctx });
+          this.logger.error(_scope, 'failed, unknown or unsupported type of stored credential', { identifier, ctx });
         }
       }
 
index bd08eef26e7cc44d11473b53bfcd3a8a1e2f6e4f..a0bea6a94ce46ae4ec785f8fdabd796b7d8ab881 100644 (file)
@@ -124,9 +124,11 @@ class SessionManager {
     }
 
     // Otherwise, carry on with IndieAuth handshake.
-    let me, session, authorizationEndpoint;
+    let me, meAutoScheme, session, authorizationEndpoint;
     try {
       me = new URL(ctx.parsedBody['me']);
+      meAutoScheme = !!ctx.parsedBody['me_auto_scheme'];
+
     } catch (e) {
       this.logger.debug(_scope, 'failed to parse supplied profile url', { ctx });
       ctx.errors.push(`Unable to understand '${ctx.parsedBody['me']}' as a profile URL.`);
@@ -134,7 +136,14 @@ class SessionManager {
 
     if (this.options.authenticator.authnEnabled.includes('indieAuth')
     &&  me) {
-      const profile = await this.indieAuthCommunication.fetchProfile(me);
+      let profile;
+      profile = await this.indieAuthCommunication.fetchProfile(me);
+      if ((!profile || !profile.metadata)
+      &&  meAutoScheme) {
+        this.logger.debug(_scope, 'trying http fallback', { ctx });
+        me.protocol = 'http';
+        profile = await this.indieAuthCommunication.fetchProfile(me);
+      }
       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}'.`);
index 7d059e0d505caf4d4fb503f554dcd91d9d05ccc3..d6a45f5be36053cc4e2b96f2ac6fde479b1fdf50 100644 (file)
@@ -15,6 +15,7 @@ function indieAuthSection(ctx, options) {
 \t\t\t\t\t\t<legend>IndieAuth</legend>
 \t\t\t\t\t\t<label for="me">Profile URL:</label>
 \t\t\t\t\t\t<input id="me" name="me" type="url" size="40" placeholder="https://example.com/my_profile_url" value="" autofocus>
+\t\t\t\t\t\t<input type="hidden" name="me_auto_scheme">
 \t\t\t\t\t\t<button>Login</button>
 ${indieAuthBlurb}
 \t\t\t\t\t</fieldset>
@@ -24,6 +25,44 @@ ${indieAuthBlurb}
 }
 
 
+/**
+ * Default all URL inputs to https if scheme not specified,
+ * and set a flag if that happened.
+ * From https://aaronparecki.com/2019/05/13/2/https
+ */
+function indieAuthURLTrySecureFirstScript(ctx, options) {
+  const showIndieAuthForm = options.authnEnabled.includes('indieAuth');
+  return showIndieAuthForm ? `
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+  function addDefaultScheme(target) {
+    if (target.value.match(/^(?!https?:).+\\..+/)) {
+      const autoSchemeField = document.querySelector('input[name=' + target.getAttribute('name') + '_auto_scheme]');
+      let scheme;
+      if (autoSchemeField) {
+        scheme = 'https://';
+        autoSchemeField.value = '1';
+      } else {
+        scheme = 'http://';
+      }
+      target.value = scheme + target.value;
+    }
+  }
+  const elements = document.querySelectorAll('input[type=url]');
+  Array.prototype.forEach.call(elements, function (el, i) {
+    el.addEventListener('blur', function(e) {
+      addDefaultScheme(e.target);
+    });
+    el.addEventListener('keydown', function(e) {
+      if (e.keyCode === 13) {
+        addDefaultScheme(e.target);
+      }
+    });
+  });
+});
+</script>` : '';
+}
+
 const userAuthn = ['argon2', 'pam', 'DEBUG_ANY'];
 function userSection(ctx, options) {
   const userBlurb = (options.userBlurb || []).map((x) => '\t'.repeat(6) + x).join('\n');
@@ -78,6 +117,7 @@ module.exports = (ctx, options) => {
   };
   const mainContent = [
     ...(options.authenticator.loginBlurb || []),
+    indieAuthURLTrySecureFirstScript(ctx, htmlOptions),
     indieAuthSection(ctx, htmlOptions),
     userSection(ctx, htmlOptions),
   ];
index bf8df03b94f8875bfa5306e6f9a59f51546cfbe6..a3efeafce3b812fcfd3961393e4e015a5986afad 100644 (file)
@@ -113,6 +113,21 @@ describe('SessionManager', function () {
       await manager.postAdminLogin(res, ctx);
       assert(!res.setHeader.called);
     });
+    it('covers profile scheme fallback', async function () {
+      ctx.parsedBody.me = 'https://example.com/profile';
+      ctx.parsedBody.me_auto_scheme = '1';
+      manager.indieAuthCommunication.fetchProfile
+        .onCall(0).resolves()
+        .onCall(1).resolves({
+        metadata: {
+          issuer: 'https://example.com/',
+          authorizationEndpoint: 'https://example.com/auth',
+        },
+      });
+      await manager.postAdminLogin(res, ctx);
+      assert.strictEqual(res.statusCode, 302);
+
+    });
     describe('living-standard-20220212', function () {
       it('covers valid profile', async function () {
         ctx.parsedBody.me = 'https://example.com/profile';