fix passkey retry, cleanup mfa set

This commit is contained in:
peintnermax
2024-09-17 10:12:03 +02:00
parent fdb2711af0
commit 424bdf42f2
4 changed files with 151 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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