mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-13 21:36:03 +00:00
Merge pull request #379 from zitadel/mfa-init-prompt
feat: MFA init prompt
This commit is contained in:
@@ -16,6 +16,26 @@ FirstInstance:
|
|||||||
ExpirationDate: 2099-01-01T00:00:00Z
|
ExpirationDate: 2099-01-01T00:00:00Z
|
||||||
|
|
||||||
DefaultInstance:
|
DefaultInstance:
|
||||||
|
LoginPolicy:
|
||||||
|
AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD
|
||||||
|
AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER
|
||||||
|
AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP
|
||||||
|
ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA
|
||||||
|
HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET
|
||||||
|
IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES
|
||||||
|
AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY
|
||||||
|
# 1 is allowed, 0 is not allowed
|
||||||
|
PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE
|
||||||
|
# DefaultRedirectURL is empty by default because we use the Console UI
|
||||||
|
DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI
|
||||||
|
# 240h = 10d
|
||||||
|
PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME
|
||||||
|
# 240h = 10d
|
||||||
|
ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME
|
||||||
|
# 720h = 30d
|
||||||
|
MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME
|
||||||
|
SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME
|
||||||
|
MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME
|
||||||
PrivacyPolicy:
|
PrivacyPolicy:
|
||||||
TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
|
TOSLink: "https://zitadel.com/docs/legal/terms-of-service"
|
||||||
PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"
|
PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy"
|
||||||
|
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"set": {
|
"set": {
|
||||||
"title": "2-Faktor einrichten",
|
"title": "2-Faktor einrichten",
|
||||||
"description": "Wählen Sie einen der folgenden zweiten Faktoren."
|
"description": "Wählen Sie einen der folgenden zweiten Faktoren.",
|
||||||
|
"skip": "Überspringen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"set": {
|
"set": {
|
||||||
"title": "Set up 2-Factor",
|
"title": "Set up 2-Factor",
|
||||||
"description": "Choose one of the following second factors."
|
"description": "Choose one of the following second factors.",
|
||||||
|
"skip": "Skip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"set": {
|
"set": {
|
||||||
"title": "Configurar autenticación de 2 factores",
|
"title": "Configurar autenticación de 2 factores",
|
||||||
"description": "Elige uno de los siguientes factores secundarios."
|
"description": "Elige uno de los siguientes factores secundarios.",
|
||||||
|
"skip": "Omitir"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"set": {
|
"set": {
|
||||||
"title": "Configura l'autenticazione a 2 fattori",
|
"title": "Configura l'autenticazione a 2 fattori",
|
||||||
"description": "Scegli uno dei seguenti secondi fattori."
|
"description": "Scegli uno dei seguenti secondi fattori.",
|
||||||
|
"skip": "Salta"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"set": {
|
"set": {
|
||||||
"title": "设置双因素认证",
|
"title": "设置双因素认证",
|
||||||
"description": "选择以下的一个第二因素。"
|
"description": "选择以下的一个第二因素。",
|
||||||
|
"skip": "跳过"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
@@ -389,7 +389,6 @@ In future, self service options to jump to are shown below, like:
|
|||||||
|
|
||||||
## Currently NOT Supported
|
## Currently NOT Supported
|
||||||
|
|
||||||
- Login Settings: multifactor init prompt
|
|
||||||
- forceMFA on login settings is not checked for IDPs
|
- forceMFA on login settings is not checked for IDPs
|
||||||
|
|
||||||
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.
|
Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced.
|
||||||
|
@@ -49,10 +49,10 @@ export default async function Page(props: {
|
|||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
const sessionWithData = sessionId
|
const sessionWithData = sessionId
|
||||||
? await loadSessionById(serviceUrl, sessionId, organization)
|
? await loadSessionById(sessionId, organization)
|
||||||
: await loadSessionByLoginname(serviceUrl, loginName, organization);
|
: await loadSessionByLoginname(loginName, organization);
|
||||||
|
|
||||||
async function getAuthMethodsAndUser(host: string, session?: Session) {
|
async function getAuthMethodsAndUser(session?: Session) {
|
||||||
const userId = session?.factors?.user?.id;
|
const userId = session?.factors?.user?.id;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -80,7 +80,6 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSessionByLoginname(
|
async function loadSessionByLoginname(
|
||||||
host: string,
|
|
||||||
loginName?: string,
|
loginName?: string,
|
||||||
organization?: string,
|
organization?: string,
|
||||||
) {
|
) {
|
||||||
@@ -92,23 +91,18 @@ export default async function Page(props: {
|
|||||||
organization,
|
organization,
|
||||||
},
|
},
|
||||||
}).then((session) => {
|
}).then((session) => {
|
||||||
return getAuthMethodsAndUser(serviceUrl, session);
|
return getAuthMethodsAndUser(session);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSessionById(
|
async function loadSessionById(sessionId: string, organization?: string) {
|
||||||
host: string,
|
|
||||||
sessionId: string,
|
|
||||||
organization?: string,
|
|
||||||
) {
|
|
||||||
const recent = await getSessionCookieById({ sessionId, organization });
|
const recent = await getSessionCookieById({ sessionId, organization });
|
||||||
return getSession({
|
return getSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
|
|
||||||
sessionId: recent.id,
|
sessionId: recent.id,
|
||||||
sessionToken: recent.token,
|
sessionToken: recent.token,
|
||||||
}).then((sessionResponse) => {
|
}).then((sessionResponse) => {
|
||||||
return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
|
return getAuthMethodsAndUser(sessionResponse.session);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +141,10 @@ export default async function Page(props: {
|
|||||||
|
|
||||||
{isSessionValid(sessionWithData).valid &&
|
{isSessionValid(sessionWithData).valid &&
|
||||||
loginSettings &&
|
loginSettings &&
|
||||||
sessionWithData && (
|
sessionWithData &&
|
||||||
|
sessionWithData.factors?.user?.id && (
|
||||||
<ChooseSecondFactorToSetup
|
<ChooseSecondFactorToSetup
|
||||||
|
userId={sessionWithData.factors?.user?.id}
|
||||||
loginName={loginName}
|
loginName={loginName}
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
@@ -158,6 +154,7 @@ export default async function Page(props: {
|
|||||||
phoneVerified={sessionWithData.phoneVerified ?? false}
|
phoneVerified={sessionWithData.phoneVerified ?? false}
|
||||||
emailVerified={sessionWithData.emailVerified ?? false}
|
emailVerified={sessionWithData.emailVerified ?? false}
|
||||||
checkAfter={checkAfter === "true"}
|
checkAfter={checkAfter === "true"}
|
||||||
|
force={force === "true"}
|
||||||
></ChooseSecondFactorToSetup>
|
></ChooseSecondFactorToSetup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session";
|
||||||
import {
|
import {
|
||||||
LoginSettings,
|
LoginSettings,
|
||||||
SecondFactorType,
|
SecondFactorType,
|
||||||
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
|
import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
userId: string;
|
||||||
loginName?: string;
|
loginName?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
@@ -17,9 +21,11 @@ type Props = {
|
|||||||
checkAfter: boolean;
|
checkAfter: boolean;
|
||||||
phoneVerified: boolean;
|
phoneVerified: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
force: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ChooseSecondFactorToSetup({
|
export function ChooseSecondFactorToSetup({
|
||||||
|
userId,
|
||||||
loginName,
|
loginName,
|
||||||
sessionId,
|
sessionId,
|
||||||
requestId,
|
requestId,
|
||||||
@@ -29,7 +35,10 @@ export function ChooseSecondFactorToSetup({
|
|||||||
checkAfter,
|
checkAfter,
|
||||||
phoneVerified,
|
phoneVerified,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
|
force,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const t = useTranslations("mfa");
|
||||||
|
const router = useRouter();
|
||||||
const params = new URLSearchParams({});
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
if (loginName) {
|
if (loginName) {
|
||||||
@@ -49,39 +58,63 @@ export function ChooseSecondFactorToSetup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
<>
|
||||||
{loginSettings.secondFactors.map((factor) => {
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
switch (factor) {
|
{loginSettings.secondFactors.map((factor) => {
|
||||||
case SecondFactorType.OTP:
|
switch (factor) {
|
||||||
return TOTP(
|
case SecondFactorType.OTP:
|
||||||
userMethods.includes(AuthenticationMethodType.TOTP),
|
return TOTP(
|
||||||
"/otp/time-based/set?" + params,
|
userMethods.includes(AuthenticationMethodType.TOTP),
|
||||||
);
|
"/otp/time-based/set?" + params,
|
||||||
case SecondFactorType.U2F:
|
);
|
||||||
return U2F(
|
case SecondFactorType.U2F:
|
||||||
userMethods.includes(AuthenticationMethodType.U2F),
|
return U2F(
|
||||||
"/u2f/set?" + params,
|
userMethods.includes(AuthenticationMethodType.U2F),
|
||||||
);
|
"/u2f/set?" + params,
|
||||||
case SecondFactorType.OTP_EMAIL:
|
);
|
||||||
return (
|
case SecondFactorType.OTP_EMAIL:
|
||||||
emailVerified &&
|
return (
|
||||||
EMAIL(
|
emailVerified &&
|
||||||
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
EMAIL(
|
||||||
"/otp/email/set?" + params,
|
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
||||||
)
|
"/otp/email/set?" + params,
|
||||||
);
|
)
|
||||||
case SecondFactorType.OTP_SMS:
|
);
|
||||||
return (
|
case SecondFactorType.OTP_SMS:
|
||||||
phoneVerified &&
|
return (
|
||||||
SMS(
|
phoneVerified &&
|
||||||
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
SMS(
|
||||||
"/otp/sms/set?" + params,
|
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
||||||
)
|
"/otp/sms/set?" + params,
|
||||||
);
|
)
|
||||||
default:
|
);
|
||||||
return null;
|
default:
|
||||||
}
|
return null;
|
||||||
})}
|
}
|
||||||
</div>
|
})}
|
||||||
|
</div>
|
||||||
|
{!force && (
|
||||||
|
<button
|
||||||
|
className="transition-all text-sm hover:text-primary-light-500 dark:hover:text-primary-dark-500"
|
||||||
|
onClick={async () => {
|
||||||
|
const resp = await skipMFAAndContinueWithNextUrl({
|
||||||
|
userId,
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
organization,
|
||||||
|
requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp?.redirect) {
|
||||||
|
return router.push(resp.redirect);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
data-testid="reset-button"
|
||||||
|
>
|
||||||
|
{t("set.skip")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ import {
|
|||||||
setPassword,
|
setPassword,
|
||||||
setUserPassword,
|
setUserPassword,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { ConnectError, create } from "@zitadel/client";
|
||||||
import { createServerTransport } from "@zitadel/client/node";
|
import { createServerTransport } from "@zitadel/client/node";
|
||||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +72,6 @@ export async function resetPassword(command: ResetPasswordCommand) {
|
|||||||
|
|
||||||
return passwordReset({
|
return passwordReset({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
|
|
||||||
userId,
|
userId,
|
||||||
urlTemplate:
|
urlTemplate:
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
@@ -267,7 +266,8 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
return { error: "Could not verify password!" };
|
return { error: "Could not verify password!" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mfaFactorCheck = checkMFAFactors(
|
const mfaFactorCheck = await checkMFAFactors(
|
||||||
|
serviceUrl,
|
||||||
session,
|
session,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
authMethods,
|
authMethods,
|
||||||
@@ -433,7 +433,7 @@ export async function checkSessionAndSetPassword({
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error: ConnectError) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
if (error.code === 7) {
|
if (error.code === 7) {
|
||||||
return { error: "Session is not valid." };
|
return { error: "Session is not valid." };
|
||||||
|
@@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
|
|||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
|
humanMFAInitSkipped,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { Duration } from "@zitadel/client";
|
import { Duration } from "@zitadel/client";
|
||||||
@@ -20,6 +21,53 @@ import {
|
|||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service";
|
||||||
|
|
||||||
|
export async function skipMFAAndContinueWithNextUrl({
|
||||||
|
userId,
|
||||||
|
requestId,
|
||||||
|
loginName,
|
||||||
|
sessionId,
|
||||||
|
organization,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
loginName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
organization?: string;
|
||||||
|
}) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
await humanMFAInitSkipped({ serviceUrl, userId });
|
||||||
|
|
||||||
|
const url =
|
||||||
|
requestId && sessionId
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
sessionId: sessionId,
|
||||||
|
requestId: requestId,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: loginName
|
||||||
|
? await getNextUrl(
|
||||||
|
{
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
},
|
||||||
|
loginSettings?.defaultRedirectUri,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
if (url) {
|
||||||
|
return { redirect: url };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function continueWithSession({
|
export async function continueWithSession({
|
||||||
requestId,
|
requestId,
|
||||||
...session
|
...session
|
||||||
|
@@ -203,7 +203,8 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
const mfaFactorCheck = checkMFAFactors(
|
const mfaFactorCheck = await checkMFAFactors(
|
||||||
|
serviceUrl,
|
||||||
session,
|
session,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
authMethodResponse.authMethodTypes,
|
authMethodResponse.authMethodTypes,
|
||||||
@@ -407,12 +408,12 @@ export async function sendVerificationRedirectWithoutCheck(
|
|||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
const loginSettings = await getLoginSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
|
|
||||||
organization: user.details?.resourceOwner,
|
organization: user.details?.resourceOwner,
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
const mfaFactorCheck = checkMFAFactors(
|
const mfaFactorCheck = await checkMFAFactors(
|
||||||
|
serviceUrl,
|
||||||
session,
|
session,
|
||||||
loginSettings,
|
loginSettings,
|
||||||
authMethodResponse.authMethodTypes,
|
authMethodResponse.authMethodTypes,
|
||||||
|
@@ -5,6 +5,7 @@ import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/passw
|
|||||||
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { getUserByID } from "./zitadel";
|
||||||
|
|
||||||
export function checkPasswordChangeRequired(
|
export function checkPasswordChangeRequired(
|
||||||
expirySettings: PasswordExpirySettings | undefined,
|
expirySettings: PasswordExpirySettings | undefined,
|
||||||
@@ -100,7 +101,8 @@ export function checkEmailVerification(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMFAFactors(
|
export async function checkMFAFactors(
|
||||||
|
serviceUrl: string,
|
||||||
session: Session,
|
session: Session,
|
||||||
loginSettings: LoginSettings | undefined,
|
loginSettings: LoginSettings | undefined,
|
||||||
authMethods: AuthenticationMethodType[],
|
authMethods: AuthenticationMethodType[],
|
||||||
@@ -188,31 +190,61 @@ export function checkMFAFactors(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: provide a way to setup passkeys on mfa page?
|
||||||
|
return { redirect: `/mfa/set?` + params };
|
||||||
|
} else if (
|
||||||
|
loginSettings?.mfaInitSkipLifetime &&
|
||||||
|
(loginSettings.mfaInitSkipLifetime.nanos > 0 ||
|
||||||
|
loginSettings.mfaInitSkipLifetime.seconds > 0) &&
|
||||||
|
!availableMultiFactors.length &&
|
||||||
|
session?.factors?.user?.id
|
||||||
|
) {
|
||||||
|
const userResponse = await getUserByID({
|
||||||
|
serviceUrl,
|
||||||
|
userId: session.factors?.user?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const humanUser =
|
||||||
|
userResponse?.user?.type.case === "human"
|
||||||
|
? userResponse?.user.type.value
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (humanUser?.mfaInitSkipped) {
|
||||||
|
const mfaInitSkippedTimestamp = timestampDate(humanUser.mfaInitSkipped);
|
||||||
|
|
||||||
|
const mfaInitSkipLifetimeMillis =
|
||||||
|
Number(loginSettings.mfaInitSkipLifetime.seconds) * 1000 +
|
||||||
|
loginSettings.mfaInitSkipLifetime.nanos / 1000000;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime();
|
||||||
|
const timeDifference = currentTime - mfaInitSkippedTime;
|
||||||
|
|
||||||
|
if (!(timeDifference > mfaInitSkipLifetimeMillis)) {
|
||||||
|
// if the time difference is smaller than the lifetime, skip the mfa setup
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the user has never skipped the mfa init but we have a setting so we redirect
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
force: "false", // this defines if the mfa is not forced in the settings and can be skipped
|
||||||
|
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestId) {
|
||||||
|
params.append("requestId", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (session.factors?.user?.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: provide a way to setup passkeys on mfa page?
|
// TODO: provide a way to setup passkeys on mfa page?
|
||||||
return { redirect: `/mfa/set?` + params };
|
return { redirect: `/mfa/set?` + params };
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement passkey setup
|
|
||||||
|
|
||||||
// else if (
|
|
||||||
// submitted.factors &&
|
|
||||||
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
|
||||||
// promptPasswordless && // if explicitly prompted due policy
|
|
||||||
// !isAlternative // escaped if password was used as an alternative method
|
|
||||||
// ) {
|
|
||||||
// const params = new URLSearchParams({
|
|
||||||
// loginName: submitted.factors.user.loginName,
|
|
||||||
// prompt: "true",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (requestId) {
|
|
||||||
// params.append("requestId", requestId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (organization) {
|
|
||||||
// params.append("organization", organization);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return router.push(`/passkey/set?` + params);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@@ -457,6 +457,21 @@ export async function getUserByID({
|
|||||||
return userService.getUserByID({ userId }, {});
|
return userService.getUserByID({ userId }, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function humanMFAInitSkipped({
|
||||||
|
serviceUrl,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const userService: Client<typeof UserService> = await createServiceForHost(
|
||||||
|
UserService,
|
||||||
|
serviceUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
return userService.humanMFAInitSkipped({ userId }, {});
|
||||||
|
}
|
||||||
|
|
||||||
export async function verifyInviteCode({
|
export async function verifyInviteCode({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
|
Reference in New Issue
Block a user