&& 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 });
}
}
}
// 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.`);
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}'.`);
\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>
}
+/**
+ * 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');
};
const mainContent = [
...(options.authenticator.loginBlurb || []),
+ indieAuthURLTrySecureFirstScript(ctx, htmlOptions),
indieAuthSection(ctx, htmlOptions),
userSection(ctx, htmlOptions),
];
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';