diff --git a/app/client/ui/errorPages.ts b/app/client/ui/errorPages.ts index b21ac106..a07e00a7 100644 --- a/app/client/ui/errorPages.ts +++ b/app/client/ui/errorPages.ts @@ -21,6 +21,7 @@ export function createErrPage(appModel: AppModel) { errPage === 'not-found' ? createNotFoundPage(appModel, errMessage) : errPage === 'access-denied' ? createForbiddenPage(appModel, errMessage) : errPage === 'account-deleted' ? createAccountDeletedPage(appModel) : + errPage === 'mfa-not-enabled' ? createMfaNotEnabledErrorPage(appModel) : createOtherErrorPage(appModel, errMessage); } @@ -81,6 +82,25 @@ export function createAccountDeletedPage(appModel: AppModel) { ]); } +/** + * Creates a page that show the user's account does not have multifactor authentication enabled, despite being needed. + */ +export function createMfaNotEnabledErrorPage(appModel: AppModel) { + document.title = t("Multi-factor authentication required{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())}); + + const searchParams = new URL(location.href).searchParams; + + return pagePanelsError(appModel, t("Multi-factor authentication required{{suffix}}", {suffix: ''}), [ + cssErrorText(t("Multi-factor-authentication is required for accessing this site, but it is not set up on your account. Please enable it and try again.")), + cssButtonWrap(bigPrimaryButtonLink( + t("Set up Multi-factor authentication"), {href: getGristConfig().mfaSettingsUrl, target: '_blank'}, testId('error-setup-mfa') + )), + cssButtonWrap(bigBasicButtonLink( + t("Try again"), {href: getSignupUrl({ nextUrl: searchParams.get("next") || "" })}, testId('error-signin') + )) + ]) +} + /** * Creates a "Page not found" page. */ diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 8779d23a..f3d66958 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -793,6 +793,9 @@ export interface GristLoadConfig { // If backend has an email service for sending notifications. notifierEnabled?: boolean; + + // The URL to the external IDP, where the user can set up Multi-factor authentication + mfaSettingsUrl?: string; } export const Features = StringUnion( diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 17ac46a3..27f4f7b3 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1273,6 +1273,11 @@ export class FlexServer implements GristServer { this.app.get('/signed-out', expressWrap((req, resp) => this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'signed-out'}}))); + // Add a static "mfa-not-enabled" page. This is where logout typically lands when GRIST_OIDC_SP_FORCE_MFA is true + // but the user hasn't configured multi-factor authentication. + this.app.get('/login/error/mfa-not-enabled', expressWrap((req, resp) => + this._sendAppPage(req, resp, {path: 'error.html', status: 401, config: {errPage: 'mfa-not-enabled'}}))); + const comment = await this._loginMiddleware.addEndpoints(this.app); this.info.push(['loginMiddlewareComment', comment]); diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 999aa5fd..0bb5d82c 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -170,7 +170,16 @@ export class OIDCConfig { if (!amr) { throw new Error('OIDCConfig: could not verify mfa status due to missing amr claim. Make sure your IDP returns it.'); } else if (!amr.includes("mfa")) { - throw new Error(`OIDCConfig: multi-factor-authentication is not enabled for ${userInfo.email}.`); + log.error(`OIDCConfig: multi-factor-authentication is not enabled for ${userInfo.email}.`); + delete mreq.session.oidc; + + // Convert absolute URL into relative, since it will be prefixed further down the line + let targetURL = new URL(targetUrl as string); + let targetUrlRelative = targetURL.pathname; + if (targetURL.searchParams.toString()) targetUrlRelative += "?" + targetURL.searchParams.toString(); + + res.redirect(`/login/error/mfa-not-enabled?next=${targetUrlRelative}`); + return; } } diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index 8c5ebf92..ade3abc8 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -24,6 +24,7 @@ import * as handlebars from 'handlebars'; import jsesc from 'jsesc'; import * as path from 'path'; import difference = require('lodash/difference'); +import * as process from "node:process"; const translate = (req: express.Request, key: string, args?: any) => req.t(`sendAppPage.${key}`, args); @@ -98,6 +99,7 @@ export function makeGristConfig(options: MakeGristConfigOptions): GristLoadConfi canCloseAccount: isAffirmative(process.env.GRIST_ACCOUNT_CLOSE), experimentalPlugins: isAffirmative(process.env.GRIST_EXPERIMENTAL_PLUGINS), notifierEnabled: server?.hasNotifier(), + mfaSettingsUrl: process.env.GRIST_OIDC_SP_MFA_SETTINGS_URL, ...extra, }; }