mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 19:42:16 +00:00
fix passkey retry, cleanup mfa set
This commit is contained in:
@@ -158,13 +158,19 @@ After updating the session, the user is signed in.
|
||||
|
||||
<img src="./screenshots/mfaset.png" alt="/mfa/set" width="400px" />
|
||||
|
||||
This page requests a webAuthN challenge for the user and updates the session afterwards.
|
||||
This page loads login Settings and the authentication methods for a user and shows setup options.
|
||||
|
||||
Requests to the APIs made:
|
||||
|
||||
- `getBrandingSettings(org?)`
|
||||
- `getLoginSettings(user.org)`
|
||||
- `getSession()`
|
||||
- `updateSession()`
|
||||
- `listAuthenticationMethodTypes()`
|
||||
- `getUserByID()`
|
||||
|
||||
When updating the session for the webAuthN challenge, we set `userVerificationRequirement` to `UserVerificationRequirement.REQUIRED` as this will request the webAuthN method as primary method to login.
|
||||
After updating the session, the user is signed in.
|
||||
If a user has already setup a certain method, a checkbox is shown alongside the button and the button is disabled.
|
||||
OTP Email and OTP SMS only show up if the user has verified email or phone.
|
||||
If the user chooses a method he is redirected to one of `/otp/time-based/set`, `/u2f/set`, `/otp/email/set`, or `/otp/sms/set`.
|
||||
At the moment, U2F methods are hidden if a method is already added on the users resource. Reasoning is that the page should only be invoked for prompts. A self service page which shows up multiple u2f factors is implemented at a later stage.
|
||||
|
||||
> NOTE: The session and therefore the user factor defines which login settings are checked for available options.
|
||||
|
||||
@@ -12,6 +12,7 @@ import BackButton from "@/ui/BackButton";
|
||||
import ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup";
|
||||
import DynamicTheme from "@/ui/DynamicTheme";
|
||||
import UserAvatar from "@/ui/UserAvatar";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
@@ -31,6 +32,28 @@ export default async function Page({
|
||||
? await loadSessionById(sessionId, organization)
|
||||
: await loadSessionByLoginname(loginName, organization);
|
||||
|
||||
async function getAuthMethodsAndUser(session?: Session) {
|
||||
const userId = session?.factors?.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw Error("Could not get user id from session");
|
||||
}
|
||||
|
||||
return listAuthenticationMethodTypes(userId).then((methods) => {
|
||||
return getUserByID(userId).then((user) => {
|
||||
const humanUser =
|
||||
user.user?.type.case === "human" ? user.user?.type.value : undefined;
|
||||
|
||||
return {
|
||||
factors: session?.factors,
|
||||
authMethods: methods.authMethodTypes ?? [],
|
||||
phoneVerified: humanUser?.phone?.isVerified ?? false,
|
||||
emailVerified: humanUser?.email?.isVerified ?? false,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSessionByLoginname(
|
||||
loginName?: string,
|
||||
organization?: string,
|
||||
@@ -39,24 +62,7 @@ export default async function Page({
|
||||
loginName,
|
||||
organization,
|
||||
}).then((session) => {
|
||||
if (session && session.factors?.user?.id) {
|
||||
const userId = session.factors.user.id;
|
||||
return listAuthenticationMethodTypes(userId).then((methods) => {
|
||||
return getUserByID(userId).then((user) => {
|
||||
const humanUser =
|
||||
user.user?.type.case === "human"
|
||||
? user.user?.type.value
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
factors: session?.factors,
|
||||
authMethods: methods.authMethodTypes ?? [],
|
||||
phoneVerified: humanUser?.phone?.isVerified ?? false,
|
||||
emailVerified: humanUser?.email?.isVerified ?? false,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
return getAuthMethodsAndUser(session);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,29 +71,15 @@ export default async function Page({
|
||||
return getSession({
|
||||
sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
if (response?.session && response.session.factors?.user?.id) {
|
||||
const userId = response.session.factors.user.id;
|
||||
return listAuthenticationMethodTypes(userId).then((methods) => {
|
||||
return getUserByID(userId).then((user) => {
|
||||
const humanUser =
|
||||
user.user?.type.case === "human"
|
||||
? user.user?.type.value
|
||||
: undefined;
|
||||
return {
|
||||
factors: response.session?.factors,
|
||||
authMethods: methods.authMethodTypes ?? [],
|
||||
phoneVerified: humanUser?.phone?.isVerified ?? false,
|
||||
emailVerified: humanUser?.email?.isVerified ?? false,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}).then((sessionResponse) => {
|
||||
return getAuthMethodsAndUser(sessionResponse.session);
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings(organization);
|
||||
const loginSettings = await getLoginSettings(organization);
|
||||
const loginSettings = await getLoginSettings(
|
||||
sessionWithData.factors?.user?.organizationId,
|
||||
);
|
||||
|
||||
return (
|
||||
<DynamicTheme branding={branding}>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import {
|
||||
LoginSettings,
|
||||
SecondFactorType,
|
||||
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
|
||||
|
||||
@@ -47,28 +50,37 @@ export default function ChooseSecondFactorToSetup({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||
{loginSettings.secondFactors.map((factor, i) => {
|
||||
return factor === 1
|
||||
? TOTP(
|
||||
{loginSettings.secondFactors.map((factor) => {
|
||||
switch (factor) {
|
||||
case SecondFactorType.OTP:
|
||||
return TOTP(
|
||||
userMethods.includes(AuthenticationMethodType.TOTP),
|
||||
"/otp/time-based/set?" + params,
|
||||
)
|
||||
: factor === 2
|
||||
? U2F(
|
||||
userMethods.includes(AuthenticationMethodType.U2F),
|
||||
"/u2f/set?" + params,
|
||||
);
|
||||
case SecondFactorType.U2F:
|
||||
return U2F(
|
||||
userMethods.includes(AuthenticationMethodType.U2F),
|
||||
"/u2f/set?" + params,
|
||||
);
|
||||
case SecondFactorType.OTP_EMAIL:
|
||||
return (
|
||||
emailVerified &&
|
||||
EMAIL(
|
||||
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
||||
"/otp/email/set?" + params,
|
||||
)
|
||||
: factor === 3 && emailVerified
|
||||
? EMAIL(
|
||||
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
||||
"/otp/email/set?" + params,
|
||||
)
|
||||
: factor === 4 && phoneVerified
|
||||
? SMS(
|
||||
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
||||
"/otp/sms/set?" + params,
|
||||
)
|
||||
: null;
|
||||
);
|
||||
case SecondFactorType.OTP_SMS:
|
||||
return (
|
||||
phoneVerified &&
|
||||
SMS(
|
||||
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
||||
"/otp/sms/set?" + params,
|
||||
)
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,19 +50,20 @@ export default function LoginPasskey({
|
||||
const pK =
|
||||
response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions
|
||||
?.publicKey;
|
||||
if (pK) {
|
||||
submitLoginAndContinue(pK)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
|
||||
if (!pK) {
|
||||
setError("Could not request passkey challenge");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return submitLoginAndContinue(pK)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
@@ -135,59 +136,57 @@ export default function LoginPasskey({
|
||||
publicKey,
|
||||
})
|
||||
.then((assertedCredential: any) => {
|
||||
if (assertedCredential) {
|
||||
const authData = new Uint8Array(
|
||||
assertedCredential.response.authenticatorData,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(
|
||||
assertedCredential.response.clientDataJSON,
|
||||
);
|
||||
const rawId = new Uint8Array(assertedCredential.rawId);
|
||||
const sig = new Uint8Array(assertedCredential.response.signature);
|
||||
const userHandle = new Uint8Array(
|
||||
assertedCredential.response.userHandle,
|
||||
);
|
||||
const data = {
|
||||
id: assertedCredential.id,
|
||||
rawId: coerceToBase64Url(rawId, "rawId"),
|
||||
type: assertedCredential.type,
|
||||
response: {
|
||||
authenticatorData: coerceToBase64Url(authData, "authData"),
|
||||
clientDataJSON: coerceToBase64Url(
|
||||
clientDataJSON,
|
||||
"clientDataJSON",
|
||||
),
|
||||
signature: coerceToBase64Url(sig, "sig"),
|
||||
userHandle: coerceToBase64Url(userHandle, "userHandle"),
|
||||
},
|
||||
};
|
||||
return submitLogin(data).then((resp) => {
|
||||
if (authRequestId && resp && resp.sessionId) {
|
||||
return router.push(
|
||||
`/login?` +
|
||||
new URLSearchParams({
|
||||
sessionId: resp.sessionId,
|
||||
authRequest: authRequestId,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const params = new URLSearchParams({});
|
||||
|
||||
if (authRequestId) {
|
||||
params.set("authRequestId", authRequestId);
|
||||
}
|
||||
if (resp?.factors?.user?.loginName) {
|
||||
params.set("loginName", resp.factors.user.loginName);
|
||||
}
|
||||
|
||||
return router.push(`/signedin?` + params);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!assertedCredential) {
|
||||
setLoading(false);
|
||||
setError("An error on retrieving passkey");
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const authData = new Uint8Array(
|
||||
assertedCredential.response.authenticatorData,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(
|
||||
assertedCredential.response.clientDataJSON,
|
||||
);
|
||||
const rawId = new Uint8Array(assertedCredential.rawId);
|
||||
const sig = new Uint8Array(assertedCredential.response.signature);
|
||||
const userHandle = new Uint8Array(
|
||||
assertedCredential.response.userHandle,
|
||||
);
|
||||
const data = {
|
||||
id: assertedCredential.id,
|
||||
rawId: coerceToBase64Url(rawId, "rawId"),
|
||||
type: assertedCredential.type,
|
||||
response: {
|
||||
authenticatorData: coerceToBase64Url(authData, "authData"),
|
||||
clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"),
|
||||
signature: coerceToBase64Url(sig, "sig"),
|
||||
userHandle: coerceToBase64Url(userHandle, "userHandle"),
|
||||
},
|
||||
};
|
||||
|
||||
return submitLogin(data).then((resp) => {
|
||||
if (authRequestId && resp && resp.sessionId) {
|
||||
return router.push(
|
||||
`/login?` +
|
||||
new URLSearchParams({
|
||||
sessionId: resp.sessionId,
|
||||
authRequest: authRequestId,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const params = new URLSearchParams({});
|
||||
|
||||
if (authRequestId) {
|
||||
params.set("authRequestId", authRequestId);
|
||||
}
|
||||
if (resp?.factors?.user?.loginName) {
|
||||
params.set("loginName", resp.factors.user.loginName);
|
||||
}
|
||||
|
||||
return router.push(`/signedin?` + params);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -245,7 +244,27 @@ export default function LoginPasskey({
|
||||
className="self-end"
|
||||
variant={ButtonVariants.Primary}
|
||||
disabled={loading}
|
||||
onClick={() => updateSessionForChallenge()}
|
||||
onClick={async () => {
|
||||
const response = await updateSessionForChallenge();
|
||||
|
||||
const pK =
|
||||
response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions
|
||||
?.publicKey;
|
||||
|
||||
if (!pK) {
|
||||
setError("Could not request passkey challenge");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return submitLoginAndContinue(pK)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user