From b01ca12e53fcd0229476ed9b815fb98d252d46d0 Mon Sep 17 00:00:00 2001 From: peintnermax Date: Tue, 30 Apr 2024 10:39:34 +0200 Subject: [PATCH] choose factor when multiple, register u2f, verify u2f --- apps/login/app/(login)/mfa/page.tsx | 82 ++++++- apps/login/app/(login)/passkey/login/page.tsx | 2 - apps/login/app/(login)/u2f/page.tsx | 76 +++++- apps/login/app/(login)/u2f/set/page.tsx | 69 ++++-- apps/login/app/api/u2f/route.ts | 50 ++++ apps/login/app/api/u2f/set/route.ts | 70 ++++++ apps/login/lib/zitadel.ts | 62 ++++- apps/login/ui/AuthMethods.tsx | 184 +++++++++++++++ apps/login/ui/ChooseSecondFactor.tsx | 68 ++++++ apps/login/ui/ChooseSecondFactorToSetup.tsx | 204 +---------------- apps/login/ui/LoginPasskey.tsx | 13 +- apps/login/ui/RegisterU2F.tsx | 216 ++++++++++++++++++ packages/zitadel-server/src/index.ts | 2 + 13 files changed, 863 insertions(+), 235 deletions(-) create mode 100644 apps/login/app/api/u2f/route.ts create mode 100644 apps/login/app/api/u2f/set/route.ts create mode 100644 apps/login/ui/AuthMethods.tsx create mode 100644 apps/login/ui/ChooseSecondFactor.tsx create mode 100644 apps/login/ui/RegisterU2F.tsx diff --git a/apps/login/app/(login)/mfa/page.tsx b/apps/login/app/(login)/mfa/page.tsx index 1f033eb4f3a..99bac281d46 100644 --- a/apps/login/app/(login)/mfa/page.tsx +++ b/apps/login/app/(login)/mfa/page.tsx @@ -1,14 +1,68 @@ -import { getBrandingSettings, server } from "#/lib/zitadel"; +import { + getBrandingSettings, + getSession, + listAuthenticationMethodTypes, + server, +} from "#/lib/zitadel"; +import Alert from "#/ui/Alert"; +import ChooseSecondFactor from "#/ui/ChooseSecondFactor"; import DynamicTheme from "#/ui/DynamicTheme"; +import UserAvatar from "#/ui/UserAvatar"; +import { + getMostRecentCookieWithLoginname, + getSessionCookieById, +} from "#/utils/cookies"; export default async function Page({ searchParams, }: { searchParams: Record; }) { - const { loginName, authRequestId, sessionId, organization, code, submit } = + const { loginName, checkAfter, authRequestId, organization, sessionId } = searchParams; + const sessionFactors = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function loadSessionByLoginname( + loginName?: string, + organization?: string + ) { + const recent = await getMostRecentCookieWithLoginname( + loginName, + organization + ); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session && response.session.factors?.user?.id) { + return listAuthenticationMethodTypes( + response.session.factors.user.id + ).then((methods) => { + return { + factors: response.session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); + } + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById(sessionId, organization); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session && response.session.factors?.user?.id) { + return listAuthenticationMethodTypes( + response.session.factors.user.id + ).then((methods) => { + return { + factors: response.session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); + } + }); + } + const branding = await getBrandingSettings(server, organization); return ( @@ -18,7 +72,29 @@ export default async function Page({

Choose one of the following second factors.

-
+ {sessionFactors && ( + + )} + + {!(loginName || sessionId) && ( + Provide your active session as loginName param + )} + + {sessionFactors ? ( + + ) : ( + No second factors available to setup. + )} ); diff --git a/apps/login/app/(login)/passkey/login/page.tsx b/apps/login/app/(login)/passkey/login/page.tsx index bcc366465a9..68df9c6da54 100644 --- a/apps/login/app/(login)/passkey/login/page.tsx +++ b/apps/login/app/(login)/passkey/login/page.tsx @@ -64,8 +64,6 @@ export default async function Page({ )}

{description}

- {!sessionFactors &&
} - {!(loginName || sessionId) && ( Provide your active session as loginName param )} diff --git a/apps/login/app/(login)/u2f/page.tsx b/apps/login/app/(login)/u2f/page.tsx index 7afd8f883f7..a5136657a85 100644 --- a/apps/login/app/(login)/u2f/page.tsx +++ b/apps/login/app/(login)/u2f/page.tsx @@ -1,6 +1,17 @@ -import { getBrandingSettings, getLoginSettings, server } from "#/lib/zitadel"; +import { + getBrandingSettings, + getLoginSettings, + getSession, + server, +} from "#/lib/zitadel"; +import Alert from "#/ui/Alert"; import DynamicTheme from "#/ui/DynamicTheme"; import LoginPasskey from "#/ui/LoginPasskey"; +import UserAvatar from "#/ui/UserAvatar"; +import { + getMostRecentCookieWithLoginname, + getSessionCookieById, +} from "#/utils/cookies"; export default async function Page({ searchParams, @@ -9,25 +20,68 @@ export default async function Page({ searchParams: Record; params: Record; }) { - const { loginName, authRequestId, sessionId, organization, code, submit } = - searchParams; + const { loginName, authRequestId, sessionId, organization } = searchParams; const branding = await getBrandingSettings(server, organization); + const sessionFactors = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function loadSessionByLoginname( + loginName?: string, + organization?: string + ) { + const recent = await getMostRecentCookieWithLoginname( + loginName, + organization + ); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById(sessionId, organization); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + return (

Verify 2-Factor

-

Verify your account with your device.

+ {sessionFactors && ( + + )} +

+ Verify your account with your device. +

- + {!(loginName || sessionId) && ( + Provide your active session as loginName param + )} + + {(loginName || sessionId) && ( + + )}
); diff --git a/apps/login/app/(login)/u2f/set/page.tsx b/apps/login/app/(login)/u2f/set/page.tsx index a5c8fe52a83..8ee325dffbf 100644 --- a/apps/login/app/(login)/u2f/set/page.tsx +++ b/apps/login/app/(login)/u2f/set/page.tsx @@ -1,38 +1,81 @@ import { getBrandingSettings, getSession, server } from "#/lib/zitadel"; +import Alert, { AlertType } from "#/ui/Alert"; import DynamicTheme from "#/ui/DynamicTheme"; +import RegisterPasskey from "#/ui/RegisterPasskey"; +import RegisterU2F from "#/ui/RegisterU2F"; +import UserAvatar from "#/ui/UserAvatar"; import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; export default async function Page({ searchParams, - params, }: { searchParams: Record; - params: Record; }) { - const { loginName, organization } = searchParams; + const { loginName, organization, authRequestId } = searchParams; - const branding = await getBrandingSettings(server, organization); + const sessionFactors = await loadSession(loginName); - const session = await loadSession(loginName, organization); - - async function loadSession(loginName?: string, organization?: string) { + async function loadSession(loginName?: string) { const recent = await getMostRecentCookieWithLoginname( loginName, organization ); - return getSession(server, recent.id, recent.token).then((response) => { - return { session: response?.session, token: recent.token }; + if (response?.session) { + return response.session; + } }); } + const title = "Use your passkey to confirm it's really you"; + const description = + "Your device will ask for your fingerprint, face, or screen lock"; + + const branding = await getBrandingSettings(server, organization); return (
-

Register Device

-

- Choose a device to register for 2-Factor Authentication. -

+

{title}

+ + {sessionFactors && ( + + )} +

{description}

+ + {/* + + A passkey is an authentication method on a device like your + fingerprint, Apple FaceID or similar. + + Passwordless Authentication + + + */} + + {!sessionFactors && ( +
+ + Could not get the context of the user. Make sure to enter the + username first or provide a loginName as searchParam. + +
+ )} + + {sessionFactors?.id && ( + + )}
); diff --git a/apps/login/app/api/u2f/route.ts b/apps/login/app/api/u2f/route.ts new file mode 100644 index 00000000000..daad539e4ad --- /dev/null +++ b/apps/login/app/api/u2f/route.ts @@ -0,0 +1,50 @@ +import { + createPasskeyRegistrationLink, + getSession, + registerPasskey, + registerU2F, + server, +} from "#/lib/zitadel"; +import { getSessionCookieById } from "#/utils/cookies"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + const body = await request.json(); + if (body) { + const { sessionId } = body; + + const sessionCookie = await getSessionCookieById(sessionId); + + const session = await getSession( + server, + sessionCookie.id, + sessionCookie.token + ); + + const domain: string = request.nextUrl.hostname; + + const userId = session?.session?.factors?.user?.id; + + if (userId) { + // TODO: add org context + return createPasskeyRegistrationLink(userId, sessionCookie.token) + .then((resp) => { + const code = resp.code; + return registerU2F(userId, domain).then((resp) => { + return NextResponse.json(resp); + }); + }) + .catch((error) => { + console.error("error on creating passkey registration link"); + return NextResponse.json(error, { status: 500 }); + }); + } else { + return NextResponse.json( + { details: "could not get session" }, + { status: 500 } + ); + } + } else { + return NextResponse.json({}, { status: 400 }); + } +} diff --git a/apps/login/app/api/u2f/set/route.ts b/apps/login/app/api/u2f/set/route.ts new file mode 100644 index 00000000000..38be7023ad2 --- /dev/null +++ b/apps/login/app/api/u2f/set/route.ts @@ -0,0 +1,70 @@ +import { + SessionCookie, + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "#/utils/cookies"; +import { setSessionAndUpdateCookie } from "#/utils/session"; +import { Checks } from "@zitadel/server"; +import { NextRequest, NextResponse, userAgent } from "next/server"; + +export async function POST(request: NextRequest) { + const body = await request.json(); + + if (body) { + const { loginName, sessionId, organization, authRequestId, code, method } = + body; + + const recentPromise: Promise = sessionId + ? getSessionCookieById(sessionId).catch((error) => { + return Promise.reject(error); + }) + : loginName + ? getSessionCookieByLoginName(loginName, organization).catch((error) => { + return Promise.reject(error); + }) + : getMostRecentSessionCookie().catch((error) => { + return Promise.reject(error); + }); + + return recentPromise + .then((recent) => { + const checks: Checks = {}; + + if (method === "time-based") { + checks.totp = { + code, + }; + } else if (method === "sms") { + checks.otpSms = { + code, + }; + } else if (method === "email") { + checks.otpEmail = { + code, + }; + } + + return setSessionAndUpdateCookie( + recent, + checks, + undefined, + authRequestId + ).then((session) => { + return NextResponse.json({ + sessionId: session.id, + factors: session.factors, + challenges: session.challenges, + }); + }); + }) + .catch((error) => { + return NextResponse.json({ details: error }, { status: 500 }); + }); + } else { + return NextResponse.json( + { details: "Request body is missing" }, + { status: 400 } + ); + } +} diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts index 59f7d29ce4a..bc13e4d5957 100644 --- a/apps/login/lib/zitadel.ts +++ b/apps/login/lib/zitadel.ts @@ -1,3 +1,4 @@ +import { VerifyU2FRegistrationRequest } from "@zitadel/server"; import { GetUserByIDResponse, RegisterTOTPResponse, @@ -348,14 +349,6 @@ export async function getUserByID( return userService.getUserByID({ userId }, {}); } -export async function listHumanAuthFactors( - server: ZitadelServer, - userId: string -): Promise { - const managementService = management.getManagement(server); - return managementService.listHumanAuthFactors({ userId }, {}); -} - export async function listUsers( userName: string, organizationId: string @@ -483,16 +476,63 @@ export async function setEmail( * @returns the newly set email */ export async function createPasskeyRegistrationLink( - userId: string + userId: string, + token?: string ): Promise { - const userservice = user.getUser(server); + let userService; + if (token) { + const authConfig: ZitadelServerOptions = { + name: "zitadel login", + apiUrl: process.env.ZITADEL_API_URL ?? "", + token: token, + }; - return userservice.createPasskeyRegistrationLink({ + const sessionUser = initializeServer(authConfig); + userService = user.getUser(sessionUser); + } else { + userService = user.getUser(server); + } + + return userService.createPasskeyRegistrationLink({ userId, returnCode: {}, }); } +/** + * + * @param server + * @param userId the id of the user where the email should be set + * @param domain the domain on which the factor is registered + * @returns the newly set email + */ +export async function registerU2F( + userId: string, + domain: string +): Promise { + const userservice = user.getUser(server); + + return userservice.registerU2F({ + userId, + domain, + }); +} + +/** + * + * @param server + * @param userId the id of the user where the email should be set + * @param domain the domain on which the factor is registered + * @returns the newly set email + */ +export async function verifyU2FRegistration( + request: VerifyU2FRegistrationRequest +): Promise { + const userservice = user.getUser(server); + + return userservice.verifyU2FRegistration(request, {}); +} + /** * * @param server diff --git a/apps/login/ui/AuthMethods.tsx b/apps/login/ui/AuthMethods.tsx new file mode 100644 index 00000000000..b9748a3ef98 --- /dev/null +++ b/apps/login/ui/AuthMethods.tsx @@ -0,0 +1,184 @@ +import clsx from "clsx"; +import Link from "next/link"; +import { BadgeState, StateBadge } from "./StateBadge"; +import { CheckIcon } from "@heroicons/react/24/solid"; + +const cardClasses = (alreadyAdded: boolean) => + clsx( + "relative bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ", + alreadyAdded ? "" : "hover:shadow-lg hover:dark:bg-white/10" + ); + +export const TOTP = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + + + + {" "} + Authenticator App +
+ {alreadyAdded && ( + <> + + + )} + + ); +}; + +export const U2F = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Universal Second Factor +
+ {alreadyAdded && ( + <> + + + )} + + ); +}; + +export const EMAIL = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + + Code via Email +
+ {alreadyAdded && ( + <> + + + )} + + ); +}; + +export const SMS = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Code via SMS +
+ {alreadyAdded && ( + <> + + + )} + + ); +}; + +function Setup() { + return ( +
+ + + +
+ ); +} diff --git a/apps/login/ui/ChooseSecondFactor.tsx b/apps/login/ui/ChooseSecondFactor.tsx new file mode 100644 index 00000000000..6d24bcfb5c2 --- /dev/null +++ b/apps/login/ui/ChooseSecondFactor.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + AuthenticationMethodType, + LoginSettings, + login, +} from "@zitadel/server"; +import Link from "next/link"; +import { BadgeState, StateBadge } from "./StateBadge"; +import clsx from "clsx"; +import { CheckIcon } from "@heroicons/react/24/outline"; +import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods"; + +type Props = { + loginName?: string; + sessionId?: string; + authRequestId?: string; + organization?: string; + userMethods: AuthenticationMethodType[]; +}; + +export default function ChooseSecondFactor({ + loginName, + sessionId, + authRequestId, + organization, + userMethods, +}: Props) { + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (sessionId) { + params.append("sessionId", sessionId); + } + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + if (organization) { + params.append("organization", organization); + } + + return ( +
+ {userMethods.map((method, i) => { + return ( +
+ {method === 4 && TOTP(false, "")} + {method === 2 && U2F(false, "")} + {method === 3 && EMAIL(false, "")} + {method === 4 && SMS(false, "")} +
+ ); + })} +
+ ); +} + +function Setup() { + return ( +
+ + + +
+ ); +} diff --git a/apps/login/ui/ChooseSecondFactorToSetup.tsx b/apps/login/ui/ChooseSecondFactorToSetup.tsx index d90f7838c4b..d529a6834da 100644 --- a/apps/login/ui/ChooseSecondFactorToSetup.tsx +++ b/apps/login/ui/ChooseSecondFactorToSetup.tsx @@ -9,6 +9,7 @@ import Link from "next/link"; import { BadgeState, StateBadge } from "./StateBadge"; import clsx from "clsx"; import { CheckIcon } from "@heroicons/react/24/outline"; +import { EMAIL, SMS, TOTP, U2F } from "./AuthMethods"; type Props = { loginName?: string; @@ -29,12 +30,6 @@ export default function ChooseSecondFactorToSetup({ userMethods, checkAfter, }: Props) { - const cardClasses = (alreadyAdded: boolean) => - clsx( - "relative bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ", - alreadyAdded ? "" : "hover:shadow-lg hover:dark:bg-white/10" - ); - const params = new URLSearchParams({}); if (loginName) { @@ -53,201 +48,24 @@ export default function ChooseSecondFactorToSetup({ params.append("checkAfter", "true"); } - const TOTP = (alreadyAdded: boolean) => { - return ( - -
- - - - - - - {" "} - Authenticator App -
- {alreadyAdded && ( - <> - - - )} - - ); - }; - - const U2F = (alreadyAdded: boolean) => { - return ( - -
- - - - Universal Second Factor -
- {alreadyAdded && ( - <> - - - )} - - ); - }; - - const EMAIL = (alreadyAdded: boolean) => { - return ( - -
- - - - - Code via Email -
- {alreadyAdded && ( - <> - - - )} - - ); - }; - - const SMS = (alreadyAdded: boolean) => { - return ( - -
- - - - Code via SMS -
- {alreadyAdded && ( - <> - - - )} - - ); - }; - return (
{loginSettings.secondFactors.map((factor, i) => { return (
- {factor === 1 && TOTP(userMethods.includes(4))} - {factor === 2 && U2F(userMethods.includes(5))} - {factor === 3 && EMAIL(userMethods.includes(7))} - {factor === 4 && SMS(userMethods.includes(6))} + {factor === 1 && + TOTP( + userMethods.includes(4), + userMethods.includes(4) ? "" : "/otp/time-based/set?" + params + )} + {factor === 2 && U2F(userMethods.includes(5), "/u2f/set?" + params)} + {factor === 3 && + EMAIL(userMethods.includes(7), "/otp/email/set?" + params)} + {factor === 4 && + SMS(userMethods.includes(6), "/otp/sms/set?" + params)}
); })}
); } - -function Setup() { - return ( -
- - - -
- ); -} diff --git a/apps/login/ui/LoginPasskey.tsx b/apps/login/ui/LoginPasskey.tsx index fb8a024214b..84d49011122 100644 --- a/apps/login/ui/LoginPasskey.tsx +++ b/apps/login/ui/LoginPasskey.tsx @@ -14,6 +14,7 @@ type Props = { sessionId?: string; authRequestId?: string; altPassword: boolean; + login?: boolean; organization?: string; }; @@ -23,6 +24,7 @@ export default function LoginPasskey({ authRequestId, altPassword, organization, + login = true, }: Props) { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -31,6 +33,7 @@ export default function LoginPasskey({ const initialized = useRef(false); + // TODO: move this to server side useEffect(() => { if (!initialized.current) { initialized.current = true; @@ -61,7 +64,9 @@ export default function LoginPasskey({ } }, []); - async function updateSessionForChallenge() { + async function updateSessionForChallenge( + userVerificationRequirement: number = login ? 1 : 3 + ) { setLoading(true); const res = await fetch("/api/session", { method: "PUT", @@ -75,7 +80,11 @@ export default function LoginPasskey({ challenges: { webAuthN: { domain: "", - userVerificationRequirement: 1, + // USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; + // USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login + // USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; + // USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa + userVerificationRequirement: userVerificationRequirement, }, }, authRequestId, diff --git a/apps/login/ui/RegisterU2F.tsx b/apps/login/ui/RegisterU2F.tsx new file mode 100644 index 00000000000..7fb2d2cd16c --- /dev/null +++ b/apps/login/ui/RegisterU2F.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState } from "react"; +import { Button, ButtonVariants } from "./Button"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { Spinner } from "./Spinner"; +import Alert from "./Alert"; +import { AuthRequest, RegisterPasskeyResponse } from "@zitadel/server"; +import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; +type Inputs = {}; + +type Props = { + sessionId: string; + authRequestId?: string; + organization?: string; +}; + +export default function RegisterU2F({ + sessionId, + organization, + authRequestId, +}: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitRegister() { + setError(""); + setLoading(true); + const res = await fetch("/api/u2f", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sessionId, + }), + }); + + const response = await res.json(); + + setLoading(false); + if (!res.ok) { + setError(response.details); + return Promise.reject(response.details); + } + return response; + } + + async function submitVerify( + passkeyId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string + ) { + setLoading(true); + const res = await fetch("/api/u2f/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + passkeyId, + passkeyName, + publicKeyCredential, + sessionId, + }), + }); + + const response = await res.json(); + + setLoading(false); + if (!res.ok) { + setError(response.details); + return Promise.reject(response.details); + } + return response; + } + + function submitRegisterAndContinue(value: Inputs): Promise { + return submitRegister().then((resp: RegisterPasskeyResponse) => { + const passkeyId = resp.passkeyId; + + if ( + resp.publicKeyCredentialCreationOptions && + resp.publicKeyCredentialCreationOptions.publicKey + ) { + resp.publicKeyCredentialCreationOptions.publicKey.challenge = + coerceToArrayBuffer( + resp.publicKeyCredentialCreationOptions.publicKey.challenge, + "challenge" + ); + resp.publicKeyCredentialCreationOptions.publicKey.user.id = + coerceToArrayBuffer( + resp.publicKeyCredentialCreationOptions.publicKey.user.id, + "userid" + ); + if ( + resp.publicKeyCredentialCreationOptions.publicKey.excludeCredentials + ) { + resp.publicKeyCredentialCreationOptions.publicKey.excludeCredentials.map( + (cred: any) => { + cred.id = coerceToArrayBuffer( + cred.id as string, + "excludeCredentials.id" + ); + return cred; + } + ); + } + + navigator.credentials + .create(resp.publicKeyCredentialCreationOptions) + .then((resp) => { + if ( + resp && + (resp as any).response.attestationObject && + (resp as any).response.clientDataJSON && + (resp as any).rawId + ) { + const attestationObject = (resp as any).response + .attestationObject; + const clientDataJSON = (resp as any).response.clientDataJSON; + const rawId = (resp as any).rawId; + + const data = { + id: resp.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: resp.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject" + ), + clientDataJSON: coerceToBase64Url( + clientDataJSON, + "clientDataJSON" + ), + }, + }; + return submitVerify(passkeyId, "", data, sessionId).then(() => { + const params = new URLSearchParams(); + + if (organization) { + params.set("organization", organization); + } + + if (authRequestId) { + params.set("authRequestId", authRequestId); + params.set("sessionId", sessionId); + // params.set("altPassword", ${false}); // without setting altPassword this does not allow password + // params.set("loginName", resp.loginName); + + router.push("/u2f?" + params); + } else { + router.push("/accounts?" + params); + } + }); + } else { + setLoading(false); + setError("An error on registering passkey"); + return null; + } + }) + .catch((error) => { + console.error(error); + setLoading(false); + setError(error); + + return null; + }); + } + }); + } + + const { errors } = formState; + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + + + +
+
+ ); +} diff --git a/packages/zitadel-server/src/index.ts b/packages/zitadel-server/src/index.ts index c8f8ddbe47b..3ce17619fab 100644 --- a/packages/zitadel-server/src/index.ts +++ b/packages/zitadel-server/src/index.ts @@ -94,6 +94,8 @@ export { RegisterTOTPResponse, VerifyTOTPRegistrationRequest, VerifyTOTPRegistrationResponse, + VerifyU2FRegistrationRequest, + VerifyU2FRegistrationResponse, } from "./proto/server/zitadel/user/v2beta/user_service"; export { AuthFactor } from "./proto/server/zitadel/user";