diff --git a/apps/login/app/passkeys/route.ts b/apps/login/app/passkeys/route.ts index 394e7ae4972..8f511627013 100644 --- a/apps/login/app/passkeys/route.ts +++ b/apps/login/app/passkeys/route.ts @@ -1,6 +1,7 @@ import { createPasskeyRegistrationLink, getSession, + registerPasskey, server, } from "#/lib/zitadel"; import { getSessionCookieById } from "#/utils/cookies"; @@ -12,7 +13,6 @@ export async function POST(request: NextRequest) { const { sessionId } = body; const sessionCookie = await getSessionCookieById(sessionId); - console.log(sessionCookie); const session = await getSession( server, @@ -20,14 +20,15 @@ export async function POST(request: NextRequest) { sessionCookie.token ); - if (session?.session && session.session?.factors?.user?.id) { - console.log(session.session.factors.user.id, sessionCookie.token); - return createPasskeyRegistrationLink( - session.session.factors.user.id, - sessionCookie.token - ) + const userId = session?.session?.factors?.user?.id; + + if (userId) { + return createPasskeyRegistrationLink(userId, sessionCookie.token) .then((resp) => { - return NextResponse.json(resp); + const code = resp.code; + return registerPasskey(userId, code).then((resp) => { + return NextResponse.json(resp); + }); }) .catch((error) => { return NextResponse.json(error, { status: 500 }); diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts index fbb5043396d..8430e10765d 100644 --- a/apps/login/lib/zitadel.ts +++ b/apps/login/lib/zitadel.ts @@ -251,7 +251,7 @@ export async function verifyPasskeyRegistration( */ export async function registerPasskey( userId: string, - sessionToken: string + code: { id: string; code: string } ): Promise { // this actions will be made from the currently seleected user const zitadelConfig: ZitadelServerOptions = { @@ -263,13 +263,11 @@ export async function registerPasskey( const authserver: ZitadelServer = initializeServer(zitadelConfig); console.log("server", authserver); const userservice = user.getUser(server); - return userservice.registerPasskey( - { - userId, - // returnCode: new ReturnPasskeyRegistrationCode(), - }, - { metadata: bearerTokenMetadata(sessionToken) } - ); + return userservice.registerPasskey({ + userId, + code, + // returnCode: new ReturnPasskeyRegistrationCode(), + }); } export { server }; diff --git a/apps/login/ui/RegisterPasskey.tsx b/apps/login/ui/RegisterPasskey.tsx index fcca2807507..98267c43fc7 100644 --- a/apps/login/ui/RegisterPasskey.tsx +++ b/apps/login/ui/RegisterPasskey.tsx @@ -6,7 +6,8 @@ import { useForm } from "react-hook-form"; import { useRouter } from "next/navigation"; import { Spinner } from "./Spinner"; import Alert from "./Alert"; - +import { RegisterPasskeyResponse } from "@zitadel/server"; +import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; type Inputs = {}; type Props = { @@ -50,8 +51,120 @@ export default function RegisterPasskey({ sessionId }: Props) { } function submitRegisterAndContinue(value: Inputs): Promise { - return submitRegister().then((resp: any) => { - return router.push(`/accounts`); + return submitRegister().then((resp: RegisterPasskeyResponse) => { + console.log(resp.publicKeyCredentialCreationOptions?.publicKey); + 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, + "challenge" + ); + 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) => { + console.log(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 = JSON.stringify({ + id: resp.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: resp.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject" + ), + clientDataJSON: coerceToBase64Url( + clientDataJSON, + "clientDataJSON" + ), + }, + }); + + const base64 = btoa(data); + + return base64; + // if (this.type === U2FComponentDestination.MFA) { + // this.service + // .verifyMyMultiFactorU2F(base64, this.name) + // .then(() => { + // this.translate + // .get("USER.MFA.U2F_SUCCESS") + // .pipe(take(1)) + // .subscribe((msg) => { + // this.toast.showInfo(msg); + // }); + // this.dialogRef.close(true); + // this.loading = false; + // }) + // .catch((error) => { + // this.loading = false; + // this.toast.showError(error); + // }); + // } else if (this.type === U2FComponentDestination.PASSWORDLESS) { + // this.service + // .verifyMyPasswordless(base64, this.name) + // .then(() => { + // this.translate + // .get("USER.PASSWORDLESS.U2F_SUCCESS") + // .pipe(take(1)) + // .subscribe((msg) => { + // this.toast.showInfo(msg); + // }); + // this.dialogRef.close(true); + // this.loading = false; + // }) + // .catch((error) => { + // this.loading = false; + // this.toast.showError(error); + // }); + // } + } else { + setLoading(false); + setError("An error on registering passkey"); + return null; + } + }) + .catch((error) => { + console.error(error); + setLoading(false); + // setError(error); + + return null; + }); + } + // return router.push(`/accounts`); }); } diff --git a/apps/login/utils/base64.ts b/apps/login/utils/base64.ts new file mode 100644 index 00000000000..967cdc8d17a --- /dev/null +++ b/apps/login/utils/base64.ts @@ -0,0 +1,63 @@ +export function coerceToBase64Url(thing: any, name: string) { + // Array or ArrayBuffer to Uint8Array + if (Array.isArray(thing)) { + thing = Uint8Array.from(thing); + } + + if (thing instanceof ArrayBuffer) { + thing = new Uint8Array(thing); + } + + // Uint8Array to base64 + if (thing instanceof Uint8Array) { + var str = ""; + var len = thing.byteLength; + + for (var i = 0; i < len; i++) { + str += String.fromCharCode(thing[i]); + } + thing = window.btoa(str); + } + + if (typeof thing !== "string") { + throw new Error("could not coerce '" + name + "' to string"); + } + + // base64 to base64url + // NOTE: "=" at the end of challenge is optional, strip it off here + thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); + + return thing; +} + +export function coerceToArrayBuffer(thing: any, name: string) { + if (typeof thing === "string") { + // base64url to base64 + thing = thing.replace(/-/g, "+").replace(/_/g, "/"); + + // base64 to Uint8Array + var str = window.atob(thing); + var bytes = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + thing = bytes; + } + + // Array to Uint8Array + if (Array.isArray(thing)) { + thing = new Uint8Array(thing); + } + + // Uint8Array to ArrayBuffer + if (thing instanceof Uint8Array) { + thing = thing.buffer; + } + + // error if none of the above worked + if (!(thing instanceof ArrayBuffer)) { + throw new TypeError("could not coerce '" + name + "' to ArrayBuffer"); + } + + return thing; +} diff --git a/packages/zitadel-server/src/index.ts b/packages/zitadel-server/src/index.ts index eb684cd0dd2..e7b6d2e77af 100644 --- a/packages/zitadel-server/src/index.ts +++ b/packages/zitadel-server/src/index.ts @@ -29,6 +29,7 @@ export { AddHumanUserResponse, VerifyEmailResponse, VerifyPasskeyRegistrationResponse, + RegisterPasskeyResponse, } from "./proto/server/zitadel/user/v2alpha/user_service"; export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";