X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;ds=sidebyside;f=lib%2Fsession-manager.js;h=43990cc376e4ee8b16a35092c2bf81620b3a1b9a;hb=29aaaee5ec2834fa363a8b9ea57bdc1362da4edc;hp=f7cd33a1dcf31ac13104255610b2a0b01244b3b2;hpb=dd173e6b450cbba8100883514610c9fde83d050a;p=squeep-authentication-module diff --git a/lib/session-manager.js b/lib/session-manager.js index f7cd33a..43990cc 100644 --- a/lib/session-manager.js +++ b/lib/session-manager.js @@ -6,6 +6,7 @@ const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper'); const { MysteryBox } = require('@squeep/mystery-box'); +const { randomUUID } = require('crypto'); const common = require('./common'); const Enum = require('./enum'); const Template = require('./template'); @@ -19,19 +20,21 @@ 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 + * @param {String} options.dingus.proxyPrefix + * @param {String} options.dingus.selfBaseUrl */ constructor(logger, authenticator, options) { this.logger = logger; this.authenticator = authenticator; this.options = options; this.indieAuthCommunication = new IndieAuthCommunication(logger, options); - this.mysteryBox = new MysteryBox(logger, options); + this.mysteryBox = new MysteryBox(options); + this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name))); - this.cookieLifespan = 60 * 60 * 24 * 32; + this.cookieLifespan = options.authenticator.inactiveSessionLifespanSeconds || 60 * 60 * 24 * 32; } @@ -50,6 +53,7 @@ class SessionManager { const cookieParts = [ `${cookieName}=${secureSession}`, 'HttpOnly', + 'SameSite=Lax', ]; if (this.options.authenticator.secureAuthOnly) { cookieParts.push('Secure'); @@ -121,9 +125,12 @@ class SessionManager { return; } - let me, session, authorizationEndpoint; + // Otherwise, carry on with IndieAuth handshake. + 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.`); @@ -131,29 +138,60 @@ class SessionManager { if (this.options.authenticator.authnEnabled.includes('indieAuth') && me) { - const profile = await this.indieAuthCommunication.fetchProfile(me); - if (!profile || !profile.authorizationEndpoint) { + let profile; + profile = await this.indieAuthCommunication.fetchProfile(me); + if ((!profile?.metadata) + && meAutoScheme) { + this.logger.debug(_scope, 'trying http fallback', { ctx }); + me.protocol = 'http'; + profile = await this.indieAuthCommunication.fetchProfile(me); + } + if (!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(); + + const state = randomUUID(); session = { authorizationEndpoint: authorizationEndpoint.href, - state: ctx.requestId, + state, codeVerifier: pkce.codeVerifier, me, redirect, + issuer: profile.metadata.issuer, }; + // Update auth endpoint parameters Object.entries({ 'response_type': 'code', 'client_id': this.options.dingus.selfBaseUrl, @@ -189,7 +227,7 @@ class SessionManager { const _scope = _fileScope('getAdminLogout'); this.logger.debug(_scope, 'called', { ctx }); - this._sessionCookieSet(res, '', 0); + await this._sessionCookieSet(res, '', 0); const redirect = ctx.queryParams['r'] || './'; @@ -203,6 +241,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 +269,7 @@ class SessionManager { } // Validate unpacked session values + // ... // Add any auth errors if (ctx.queryParams['error']) { @@ -251,6 +291,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 +324,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 { @@ -306,4 +358,4 @@ class SessionManager { } -module.exports = SessionManager; \ No newline at end of file +module.exports = SessionManager;