-
-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ability to force MFA on OIDC Logins #971
base: main
Are you sure you want to change the base?
Changes from all commits
39bf909
cdd0a5d
b51bfe2
7f2e3ed
1d5383e
95b387f
a113681
41806e3
d165e36
273cad5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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,32 @@ 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.")), | ||||||||||||||||||||||
Comment on lines
+96
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest avoiding the
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The plus should work as well, you're right. I'll test it later this week. |
||||||||||||||||||||||
cssButtonWrap(bigPrimaryButtonLink( | ||||||||||||||||||||||
t("Set up Multi-factor authentication"), | ||||||||||||||||||||||
{href: getGristConfig().mfaSettingsUrl, target: '_blank'}, | ||||||||||||||||||||||
testId('error-setup-mfa') | ||||||||||||||||||||||
)), | ||||||||||||||||||||||
Comment on lines
+98
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of showing that property only when
Suggested change
|
||||||||||||||||||||||
cssButtonWrap(bigBasicButtonLink( | ||||||||||||||||||||||
t("Try again"), | ||||||||||||||||||||||
{href: getSignupUrl({ nextUrl: searchParams.get("next") || "" })}, | ||||||||||||||||||||||
testId('error-signin') | ||||||||||||||||||||||
)) | ||||||||||||||||||||||
]); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
/** | ||||||||||||||||||||||
* Creates a "Page not found" page. | ||||||||||||||||||||||
*/ | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you agree with my suggestion above, don't forget to adapt the comment here :). |
||
* 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) { | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In another PR, I made something that may be of some help :
I hope my PR will be merged soon and that you could benefit from the changes. You could still create another error page specifically for MFA. However, I do not take advantage of the |
||
} | ||
} | ||
|
||
const profile = this._makeUserProfileFromUserInfo(userInfo); | ||
log.info(`OIDCConfig: got OIDC response for ${profile.email} (${profile.name}) redirecting to ${targetUrl}`); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the localization, but could you do them through Weblate instead, please: |
||
}, | ||
"menus": { | ||
"* Workspaces are available on team plans. ": "* Arbeitsbereiche sind in Teamplänen verfügbar. ", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking: you can otherwise use the
URLSearchParams
class: