m.uowis 2 weeks ago committed by GitHub
commit 40a2a3a340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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.
*/

@ -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(

@ -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]);

@ -35,6 +35,14 @@
* env GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED
* If set to "true", the user will be allowed to login even if the email is not verified by the IDP.
* Defaults to false.
* env GRIST_OIDC_SP_FORCE_MFA
* If set to "true", the user will be forced to have multi-factor authentication enabled. The state of MFA will
* be determined by OIDC's amr claim: It must include "mfa". Make sure that the IDP returns the amr claim
* correctly, otherwise authentication will fail.
* env GRIST_OIDC_SP_MFA_SETTINGS_URL
* This is needed when GRIST_OIDC_SP_FORCE_MFA is set to true. Enter the URL where the user will be able to
* configure Multi-factor authentication on their account. This will be shown in the UI if the user does not have
* MFA enabled.
*
* This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions
* at:
@ -69,6 +77,7 @@ export class OIDCConfig {
private _endSessionEndpoint: string;
private _skipEndSessionEndpoint: boolean;
private _ignoreEmailVerified: boolean;
private _forceMfa: boolean;
public constructor() {
}
@ -113,6 +122,11 @@ export class OIDCConfig {
defaultValue: false,
})!;
this._forceMfa = section.flag('forceMfa').readBool({
envVar: 'GRIST_OIDC_SP_FORCE_MFA',
defaultValue: false,
})!;
const issuer = await Issuer.discover(issuerUrl);
this._redirectUrl = new URL(CALLBACK_URL, spHost).href;
this._client = new issuer.Client({
@ -159,6 +173,26 @@ export class OIDCConfig {
throw new Error(`OIDCConfig: email not verified for ${userInfo.email}`);
}
const amr = tokenSet.claims().amr;
if (this._forceMfa && (!amr || !amr.includes("mfa"))) {
if (!amr) {
throw new Error('OIDCConfig: could not verify mfa status due to missing amr claim.');
} else if (!amr.includes("mfa")) {
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
const 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;
}
}
const profile = this._makeUserProfileFromUserInfo(userInfo);
log.info(`OIDCConfig: got OIDC response for ${profile.email} (${profile.name}) redirecting to ${targetUrl}`);

@ -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,
};
}

@ -948,7 +948,11 @@
"An unknown error occurred.": "Ein unbekannter Fehler ist aufgetreten.",
"Powered by": "Angetrieben durch",
"Build your own form": "Erstellen Sie Ihr eigenes Formular",
"Form not found": "Formular nicht gefunden"
"Form not found": "Formular nicht gefunden",
"Multi-factor authentication required{{suffix}}": "Multi-Faktor-Authentifizierung nötig{{suffix}}",
"Multi-factor-authentication is required for accessing this site, but it is not set up on your account. Please enable it and try again.": "Für den Zugriff auf diese Website ist Multi-Faktor-Authentifizierung erforderlich. Diese ist nicht in Ihrem Konto eingerichtet. Bitte richten Sie sie ein und versuchen Sie es erneut.",
"Set up Multi-factor authentication": "Multi-Faktor-Authentifizierung einrichten",
"Try again": "Erneut versuchen"
},
"menus": {
"* Workspaces are available on team plans. ": "* Arbeitsbereiche sind in Teamplänen verfügbar. ",

@ -888,7 +888,11 @@
"An unknown error occurred.": "An unknown error occurred.",
"Build your own form": "Build your own form",
"Form not found": "Form not found",
"Powered by": "Powered by"
"Powered by": "Powered by",
"Multi-factor authentication required{{suffix}}": "Multi-factor authentication required{{suffix}}",
"Multi-factor-authentication is required for accessing this site, but it is not set up on your account. Please enable it and try again.": "Multi-factor-authentication is required for accessing this site, but it is not set up on your account. Please enable it and try again.",
"Set up Multi-factor authentication": "Set up Multi-factor authentication",
"Try again": "Try again"
},
"menus": {
"* Workspaces are available on team plans. ": "* Workspaces are available on team plans. ",

Loading…
Cancel
Save