X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=src%2Fsession-manager.js;fp=src%2Fsession-manager.js;h=0000000000000000000000000000000000000000;hb=3c547e314b79a31fb3f15412a47707a22dc3eefd;hp=5caff347386fbd1ce950b7cb0dcf324a94bc048d;hpb=4807a77eca2858e8dc23d9ec2247a778814988d7;p=websub-hub diff --git a/src/session-manager.js b/src/session-manager.js deleted file mode 100644 index 5caff34..0000000 --- a/src/session-manager.js +++ /dev/null @@ -1,279 +0,0 @@ -'use strict'; - -/** - * Here we process activities which support login sessions. - */ - -const { Communication: IndieAuthCommunication } = require('@squeep/indieauth-helper'); -const { MysteryBox } = require('@squeep/mystery-box'); -const common = require('./common'); -const Enum = require('./enum'); -const Template = require('./template'); - -const _fileScope = common.fileScope(__filename); - -class SessionManager { - 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.secureCookie = options.authenticator.secureAuthOnly ? ' Secure;' : ''; - this.cookieLifespan = 60 * 60 * 24 * 32; - } - - - /** - * Set or update our session cookie. - * @param {http.ServerResponse} res - * @param {Object} session - * @param {Number} maxAge - */ - async _sessionCookieSet(res, session, maxAge) { - const cookieName = 'WSHas'; - const secureSession = session && await this.mysteryBox.pack(session) || ''; - const cookie = [ - `${cookieName}=${secureSession}`, - 'HttpOnly', - this.secureCookie, - `Max-Age: ${maxAge}`, - ].join('; '); - res.setHeader(Enum.Header.SetCookie, cookie); - } - - - /** - * GET request for establishing admin session. - * @param {http.ServerResponse} res - * @param {Object} ctx - */ - async getAdminLogin(res, ctx) { - const _scope = _fileScope('getAdminLogin'); - this.logger.debug(_scope, 'called', { ctx }); - - res.end(Template.adminLoginHTML(ctx, this.options)); - this.logger.info(_scope, 'finished', { ctx }) - } - - - /** - * POST request for taking form data to establish admin session. - * @param {http.ServerResponse} res - * @param {Object} ctx - */ - async postAdminLogin(res, ctx) { - const _scope = _fileScope('postAdminLogin'); - this.logger.debug(_scope, 'called', { ctx }); - - ctx.errors = []; - - // Only attempt user login if no IndieAuth profile is set - if (!ctx.parsedBody['me']) { - this.logger.debug(_scope, 'no indieauth profile, trying identifier', { ctx }); - - const identifier = ctx.parsedBody['identifier']; - const credential = ctx.parsedBody['credential']; - - const isValidLocalIdentifier = await this.authenticator.isValidIdentifierCredential(identifier, credential, ctx); - if (!isValidLocalIdentifier) { - ctx.errors.push('Invalid username or password'); - } - - if (ctx.errors.length) { - res.end(Template.adminLoginHTML(ctx, this.options)); - return; - } - - // Valid auth, persist the authenticated session - ctx.session = { - authenticatedIdentifier: ctx.authenticationId, - }; - await this._sessionCookieSet(res, ctx.session, this.cookieLifespan); - res.statusCode = 302; - res.setHeader(Enum.Header.Location, './'); - res.end(); - this.logger.info(_scope, 'finished local', { ctx }); - return; - } - - let me, session, authorizationEndpoint; - try { - me = new URL(ctx.parsedBody['me']); - } 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 (me) { - const profile = await this.indieAuthCommunication.fetchProfile(me); - if (!profile || !profile.authorizationEndpoint) { - 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); - } catch (e) { - ctx.errors.push(`Unable to understand the authorization endpoint ('${profile.authorizationEndpoint}') indicated by that profile ('${me}') as a URL.`); - } - } - - if (authorizationEndpoint) { - const pkce = await IndieAuthCommunication.generatePKCE(); - session = { - authorizationEndpoint: authorizationEndpoint.href, - state: ctx.requestId, - codeVerifier: pkce.codeVerifier, - me, - }; - - Object.entries({ - 'response_type': 'code', - 'client_id': this.options.dingus.selfBaseUrl, - 'redirect_uri': `${this.options.dingus.selfBaseUrl}admin/_ia`, - 'state': session.state, - 'code_challenge': pkce.codeChallenge, - 'code_challenge_method': pkce.codeChallengeMethod, - 'me': me, - }).forEach(([name, value]) => authorizationEndpoint.searchParams.set(name, value)); - } - } - - if (ctx.errors.length) { - res.end(Template.adminLoginHTML(ctx, this.options)); - return; - } - - await this._sessionCookieSet(res, session, this.cookieLifespan); - res.setHeader(Enum.Header.Location, authorizationEndpoint.href); - res.statusCode = 302; // Found - res.end(); - - this.logger.info(_scope, 'finished indieauth', { ctx }) - } - - - /** - * GET request to remove current credentials. - * @param {http.ServerResponse} res - * @param {Object} ctx - */ - async getAdminLogout(res, ctx) { - const _scope = _fileScope('getAdminLogout'); - this.logger.debug(_scope, 'called', { ctx }); - - this._sessionCookieSet(res, '', 0); - res.statusCode = 302; - res.setHeader(Enum.Header.Location, './'); - res.end(); - - this.logger.info(_scope, 'finished', { ctx }); - } - - - /** - * GET request for returning IndieAuth redirect. - * @param {http.ServerResponse} res - * @param {Object} ctx - */ - async getAdminIA(res, ctx) { - const _scope = _fileScope('getAdminIA'); - this.logger.debug(_scope, 'called', { ctx }); - - ctx.errors = []; - ctx.session = {}; - - // Unpack cookie to restore session data - - const [ cookieName, cookieValue ] = common.splitFirst((ctx.cookie || ''), '=', ''); - if (cookieName !== 'WSHas') { - this.logger.debug(_scope, 'no cookie', { ctx }); - ctx.errors.push('missing required cookie'); - } else { - try { - ctx.session = await this.mysteryBox.unpack(cookieValue); - this.logger.debug(_scope, 'restored session from cookie', { ctx }); - } catch (e) { - this.logger.debug(_scope, 'could not unpack cookie'); - ctx.errors.push('invalid cookie'); - } - } - - // Validate unpacked session values - - // Add any auth errors - if (ctx.queryParams['error']) { - ctx.errors.push(ctx.queryParams['error']); - if (ctx.queryParams['error_description']) { - ctx.errors.push(ctx.queryParams['error_description']); - } - } - - // check stuff - if (ctx.queryParams['state'] !== ctx.session.state) { - this.logger.debug(_scope, 'state mismatch', { ctx }); - ctx.errors.push('invalid state'); - } - - const code = ctx.queryParams['code']; - if (!code) { - this.logger.debug(_scope, 'missing code', { ctx }); - ctx.errors.push('invalid code'); - } - - let redeemProfileUrl; - try { - redeemProfileUrl = new URL(ctx.session.authorizationEndpoint); - } catch (e) { - this.logger.debug(_scope, 'failed to parse restored session authorization endpoint as url', { ctx }); - ctx.errors.push('invalid cookie'); - } - let profile; - if (redeemProfileUrl) { - profile = await this.indieAuthCommunication.redeemProfileCode(redeemProfileUrl, code, ctx.session.codeVerifier, this.options.dingus.selfBaseUrl, `${this.options.dingus.selfBaseUrl}admin/_ia`); - if (!profile) { - this.logger.debug(_scope, 'no profile from code redemption', { ctx }); - ctx.errors.push('did not get a profile response from authorization endpoint code redemption'); - } else if (!profile.me) { - this.logger.debug(_scope, 'no profile me identifier from code redemption', { ctx }); - ctx.errors.push('did not get \'me\' value from authorization endpoint code redemption'); - } else if (profile.me !== ctx.session.me) { - this.logger.debug(_scope, 'mis-matched canonical me from redeemed profile', { ctx, profile }); - 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) { - 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 { - // The endpoints match, all is okay, update our records. - ctx.session.me = profile.me; - } - } - } - - if (ctx.errors.length) { - await this._sessionCookieSet(res, '', 0); - res.end(Template.adminIAHTML(ctx, this.options)); - return; - } - - // set cookie as auth valid, redirect to admin - ctx.session = { - authenticatedProfile: ctx.session.me, - }; - - await this._sessionCookieSet(res, ctx.session, this.cookieLifespan); - res.statusCode = 302; - res.setHeader(Enum.Header.Location, './'); - res.end(); - - this.logger.info(_scope, 'finished', { ctx }) - } - - -} - -module.exports = SessionManager; \ No newline at end of file