mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 08:23: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" />
|
<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:
|
Requests to the APIs made:
|
||||||
|
|
||||||
- `getBrandingSettings(org?)`
|
- `getBrandingSettings(org?)`
|
||||||
|
- `getLoginSettings(user.org)`
|
||||||
- `getSession()`
|
- `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.
|
If a user has already setup a certain method, a checkbox is shown alongside the button and the button is disabled.
|
||||||
After updating the session, the user is signed in.
|
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 ChooseSecondFactorToSetup from "@/ui/ChooseSecondFactorToSetup";
|
||||||
import DynamicTheme from "@/ui/DynamicTheme";
|
import DynamicTheme from "@/ui/DynamicTheme";
|
||||||
import UserAvatar from "@/ui/UserAvatar";
|
import UserAvatar from "@/ui/UserAvatar";
|
||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -31,6 +32,28 @@ export default async function Page({
|
|||||||
? await loadSessionById(sessionId, organization)
|
? await loadSessionById(sessionId, organization)
|
||||||
: await loadSessionByLoginname(loginName, 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(
|
async function loadSessionByLoginname(
|
||||||
loginName?: string,
|
loginName?: string,
|
||||||
organization?: string,
|
organization?: string,
|
||||||
@@ -39,24 +62,7 @@ export default async function Page({
|
|||||||
loginName,
|
loginName,
|
||||||
organization,
|
organization,
|
||||||
}).then((session) => {
|
}).then((session) => {
|
||||||
if (session && session.factors?.user?.id) {
|
return getAuthMethodsAndUser(session);
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,29 +71,15 @@ export default async function Page({
|
|||||||
return getSession({
|
return getSession({
|
||||||
sessionId: recent.id,
|
sessionId: recent.id,
|
||||||
sessionToken: recent.token,
|
sessionToken: recent.token,
|
||||||
}).then((response) => {
|
}).then((sessionResponse) => {
|
||||||
if (response?.session && response.session.factors?.user?.id) {
|
return getAuthMethodsAndUser(sessionResponse.session);
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const branding = await getBrandingSettings(organization);
|
const branding = await getBrandingSettings(organization);
|
||||||
const loginSettings = await getLoginSettings(organization);
|
const loginSettings = await getLoginSettings(
|
||||||
|
sessionWithData.factors?.user?.organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTheme branding={branding}>
|
<DynamicTheme branding={branding}>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"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 { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
|
import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods";
|
||||||
|
|
||||||
@@ -47,28 +50,37 @@ export default function ChooseSecondFactorToSetup({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
<div className="grid grid-cols-1 gap-5 w-full pt-4">
|
||||||
{loginSettings.secondFactors.map((factor, i) => {
|
{loginSettings.secondFactors.map((factor) => {
|
||||||
return factor === 1
|
switch (factor) {
|
||||||
? TOTP(
|
case SecondFactorType.OTP:
|
||||||
|
return TOTP(
|
||||||
userMethods.includes(AuthenticationMethodType.TOTP),
|
userMethods.includes(AuthenticationMethodType.TOTP),
|
||||||
"/otp/time-based/set?" + params,
|
"/otp/time-based/set?" + params,
|
||||||
)
|
);
|
||||||
: factor === 2
|
case SecondFactorType.U2F:
|
||||||
? U2F(
|
return U2F(
|
||||||
userMethods.includes(AuthenticationMethodType.U2F),
|
userMethods.includes(AuthenticationMethodType.U2F),
|
||||||
"/u2f/set?" + params,
|
"/u2f/set?" + params,
|
||||||
|
);
|
||||||
|
case SecondFactorType.OTP_EMAIL:
|
||||||
|
return (
|
||||||
|
emailVerified &&
|
||||||
|
EMAIL(
|
||||||
|
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
||||||
|
"/otp/email/set?" + params,
|
||||||
)
|
)
|
||||||
: factor === 3 && emailVerified
|
);
|
||||||
? EMAIL(
|
case SecondFactorType.OTP_SMS:
|
||||||
userMethods.includes(AuthenticationMethodType.OTP_EMAIL),
|
return (
|
||||||
"/otp/email/set?" + params,
|
phoneVerified &&
|
||||||
)
|
SMS(
|
||||||
: factor === 4 && phoneVerified
|
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
||||||
? SMS(
|
"/otp/sms/set?" + params,
|
||||||
userMethods.includes(AuthenticationMethodType.OTP_SMS),
|
)
|
||||||
"/otp/sms/set?" + params,
|
);
|
||||||
)
|
default:
|
||||||
: null;
|
return null;
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,19 +50,20 @@ export default function LoginPasskey({
|
|||||||
const pK =
|
const pK =
|
||||||
response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions
|
response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions
|
||||||
?.publicKey;
|
?.publicKey;
|
||||||
if (pK) {
|
|
||||||
submitLoginAndContinue(pK)
|
if (!pK) {
|
||||||
.then(() => {
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setError(error);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setError("Could not request passkey challenge");
|
setError("Could not request passkey challenge");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return submitLoginAndContinue(pK)
|
||||||
|
.then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
@@ -135,59 +136,57 @@ export default function LoginPasskey({
|
|||||||
publicKey,
|
publicKey,
|
||||||
})
|
})
|
||||||
.then((assertedCredential: any) => {
|
.then((assertedCredential: any) => {
|
||||||
if (assertedCredential) {
|
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 {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError("An error on retrieving passkey");
|
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) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -245,7 +244,27 @@ export default function LoginPasskey({
|
|||||||
className="self-end"
|
className="self-end"
|
||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading}
|
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" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user