+ /**
+ * Check if processing an OTP entry attempt. If not, resume login flow.
+ * If so, validate otp and establish session, else reprompt for OTP, or
+ * return to login entry after too many failures.
+ * @param {*} res
+ * @param {*} ctx
+ * @returns {Boolean} true if otp was handled, otherwise false indicates further login processing needed
+ */
+ async _otpSubmission(res, ctx) {
+ const _scope = _fileScope('_otpSubmission');
+
+ // Are we processing an OTP entry attempt?
+ const { otp, state: stateBox } = ctx.parsedBody;
+ let state;
+ try {
+ if (stateBox) {
+ state = await this.mysteryBox.unpack(stateBox);
+ }
+ } catch (e) {
+ this.logger.debug(_scope, 'failed to unpack otp state', { error: e, ctx });
+ // Ignore and continue back to main login
+ return false;
+ }
+ if (otp && state) {
+ const OTPResult = await this.authenticator.checkOTP(state, otp);
+ switch (OTPResult) {
+ case Enum.OTPResult.Valid:
+ // successful otp entry
+ // Valid auth, persist the authenticated session
+ ctx.session = {
+ authenticatedIdentifier: state.authenticationId,
+ };
+ await this._sessionCookieSet(res, ctx.session, this.cookieLifespan);
+ res.statusCode = 302;
+ res.setHeader(Enum.Header.Location, state.redirect);
+ res.end();
+ this.logger.info(_scope, 'finished otp', { ctx });
+ return true;
+
+ case Enum.OTPResult.InvalidSoftFail:
+ // retry otp entry
+ ctx.otpNeeded = true;
+ ctx.errors.push('Invalid OTP token.');
+ ctx.otpState = await this.mysteryBox.pack({
+ ...state,
+ attempt: state.attempt + 1,
+ });
+ res.end(Template.OTPHTML(ctx, this.options));
+ this.logger.info(_scope, 'finished otp, invalid, request again', { ctx });
+ return true;
+
+ case Enum.OTPResult.InvalidHardFail:
+ // return to initial login
+ this.logger.debug(_scope, 'too many otp failures', { ctx, state });
+ ctx.errors.push('Invalid OTP token, and too many failures. Try again.');
+ return false;
+
+ default:
+ throw new RangeError('Unexpected OTPResult');
+ }
+ }
+ // not in otp flow
+ return false;
+ }
+
+ /**
+ *
+ * @param {http.ServerResponse} res
+ * @param {Object} ctx
+ */
+ async _localUserAuth(res, ctx) {
+ const _scope = _fileScope('_localUserAuth');
+
+ // If Indiauth enabled and profile was submitted, defer to that.
+ if (this.options.authenticator.authnEnabled.includes('indieAuth')
+ && ctx.parsedBody['me']) {
+ return false;
+ }
+
+ const redirect = ctx.queryParams['r'] || './';
+ const identifier = ctx.parsedBody['identifier'];
+ const credential = ctx.parsedBody['credential']; // N.B. Logger must specifically mask this field from ctx.
+
+ // N.B. validity check also sets autenticationId and otpNeeded on ctx
+ 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.LoginHTML(ctx, this.options));
+ return true;
+ }
+
+ // If OTP exists for valid identifier, follow that flow.
+ if (ctx.otpNeeded) {
+ ctx.otpState = await this.mysteryBox.pack({
+ authenticatedIdentifier: ctx.authenticationId,
+ epochMs: Date.now(),
+ attempt: 0,
+ redirect,
+ });
+ res.end(Template.OTPHTML(ctx, this.options));
+ this.logger.info(_scope, 'finished local, otp required', { ctx });
+ return true;
+ }
+
+ // 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, redirect);
+ res.end();
+ this.logger.info(_scope, 'finished local, no otp', { ctx });
+ return true;
+ }
+
+