diff --git a/.changeset/quiet-sloths-lose.md b/.changeset/quiet-sloths-lose.md new file mode 100644 index 00000000..f1a00bc1 --- /dev/null +++ b/.changeset/quiet-sloths-lose.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Added passkeys authentication flow diff --git a/packages/permissionless/actions/passkeyServer/startAuthentication.ts b/packages/permissionless/actions/passkeyServer/startAuthentication.ts new file mode 100644 index 00000000..ad9acfb8 --- /dev/null +++ b/packages/permissionless/actions/passkeyServer/startAuthentication.ts @@ -0,0 +1,35 @@ +import { Base64 } from "ox" +import type { WebAuthnP256 } from "ox" +import { + type Account, + type Chain, + type Client, + type Transport, + toHex +} from "viem" +import type { PasskeyServerRpcSchema } from "../../types/passkeyServer.js" + +export type StartAuthenticationReturnType = WebAuthnP256.sign.Options & { + uuid: string +} + +export const startAuthentication = async ( + client: Client< + Transport, + Chain | undefined, + Account | undefined, + PasskeyServerRpcSchema + > +): Promise => { + const response = await client.request({ + method: "pks_startAuthentication", + params: [] + }) + + return { + challenge: toHex(Base64.toBytes(response.challenge)), + rpId: response.rpId, + userVerification: response.userVerification, + uuid: response.uuid + } +} diff --git a/packages/permissionless/actions/passkeyServer/verifyAuthentication.ts b/packages/permissionless/actions/passkeyServer/verifyAuthentication.ts new file mode 100644 index 00000000..ad4c2ae0 --- /dev/null +++ b/packages/permissionless/actions/passkeyServer/verifyAuthentication.ts @@ -0,0 +1,121 @@ +import { Base64, type WebAuthnP256 } from "ox" +// import { Base64 } from "ox" +import type { Account, Chain, Client, Hex, Transport } from "viem" +import type { PasskeyServerRpcSchema } from "../../types/passkeyServer.js" + +export type VerifyAuthenticationParameters = WebAuthnP256.sign.ReturnType & { + uuid: string +} + +export type VerifyAuthenticationReturnType = { + success: boolean + id: string + publicKey: Hex +} + +export const verifyAuthentication = async ( + client: Client< + Transport, + Chain | undefined, + Account | undefined, + PasskeyServerRpcSchema + >, + args: VerifyAuthenticationParameters +): Promise => { + const { raw, uuid } = args + + let responseAuthenticatorData: string + + if ("authenticatorData" in raw.response) { + responseAuthenticatorData = Base64.fromBytes( + new Uint8Array(raw.response.authenticatorData as ArrayBuffer), + { + url: true + } + ) + } else { + throw new Error("authenticatorData not found in the signature") + } + + let signature: string + if ("signature" in raw.response) { + signature = Base64.fromBytes( + new Uint8Array(raw.response.signature as ArrayBuffer), + { + pad: false, + url: true + } + ) + } else { + throw new Error("signature not found in the signature") + } + + let userHandle: string | undefined + if ("userHandle" in raw.response) { + userHandle = Base64.fromBytes( + new Uint8Array(raw.response.userHandle as ArrayBuffer), + { + pad: false, + url: true + } + ) + } + + const serverResponse = await client.request( + { + method: "pks_verifyAuthentication", + params: [ + { + id: raw.id, + rawId: Base64.fromBytes(new Uint8Array(raw.rawId), { + pad: false, + url: true + }), + authenticatorAttachment: raw.authenticatorAttachment as + | "cross-platform" + | "platform", + response: { + clientDataJSON: Base64.fromBytes( + new Uint8Array(raw.response.clientDataJSON), + { + pad: false, + url: true + } + ), + authenticatorData: responseAuthenticatorData, + signature, + userHandle + }, + clientExtensionResults: raw.getClientExtensionResults(), + type: raw.type as "public-key" + }, + { + uuid + } + ] + }, + { + retryCount: 0 + } + ) + + const success = Boolean(serverResponse?.success) + const id = serverResponse?.id + const publicKey = serverResponse?.publicKey + + if (typeof id !== "string") { + throw new Error("Invalid passkey id returned from server") + } + + if (typeof publicKey !== "string" || !publicKey.startsWith("0x")) { + throw new Error( + "Invalid public key returned from server - must be hex string starting with 0x" + ) + } + + return { + success, + id, + publicKey: publicKey as Hex + } +} diff --git a/packages/permissionless/actions/passkeyServer/verifyRegistration.ts b/packages/permissionless/actions/passkeyServer/verifyRegistration.ts index 3f81c46d..7e4df6bc 100644 --- a/packages/permissionless/actions/passkeyServer/verifyRegistration.ts +++ b/packages/permissionless/actions/passkeyServer/verifyRegistration.ts @@ -48,49 +48,59 @@ export const verifyRegistration = async ( } } - const serverResponse = await client.request({ - method: "pks_verifyRegistration", - params: [ - { - id: credential.id, - rawId: Base64.fromBytes(new Uint8Array(credential.raw.rawId), { - pad: false, - url: true - }), - response: { - clientDataJSON: Base64.fromBytes( - new Uint8Array(response.clientDataJSON) - ), - attestationObject: Base64.fromBytes( - new Uint8Array(response.attestationObject), + const serverResponse = await client.request( + { + method: "pks_verifyRegistration", + params: [ + { + id: credential.id, + rawId: Base64.fromBytes( + new Uint8Array(credential.raw.rawId), { + pad: false, url: true } ), - transports: - typeof response.getTransports === "function" - ? (response.getTransports() as ( - | "ble" - | "cable" - | "hybrid" - | "internal" - | "nfc" - | "smart-card" - | "usb" - )[]) - : undefined, - publicKeyAlgorithm: responsePublicKeyAlgorithm, - authenticatorData: responseAuthenticatorData + response: { + clientDataJSON: Base64.fromBytes( + new Uint8Array(response.clientDataJSON) + ), + attestationObject: Base64.fromBytes( + new Uint8Array(response.attestationObject), + { + url: true + } + ), + transports: + typeof response.getTransports === "function" + ? (response.getTransports() as ( + | "ble" + | "cable" + | "hybrid" + | "internal" + | "nfc" + | "smart-card" + | "usb" + )[]) + : undefined, + publicKeyAlgorithm: responsePublicKeyAlgorithm, + authenticatorData: responseAuthenticatorData + }, + authenticatorAttachment: credential.raw + .authenticatorAttachment as + | "cross-platform" + | "platform", + clientExtensionResults: + credential.raw.getClientExtensionResults(), + type: credential.raw.type as "public-key" }, - authenticatorAttachment: credential.raw - .authenticatorAttachment as "cross-platform" | "platform", - clientExtensionResults: - credential.raw.getClientExtensionResults(), - type: credential.raw.type as "public-key" - }, - context - ] - }) + context + ] + }, + { + retryCount: 0 + } + ) const success = Boolean(serverResponse?.success) const id = serverResponse?.id diff --git a/packages/permissionless/clients/decorators/passkeyServer.ts b/packages/permissionless/clients/decorators/passkeyServer.ts index af1a0191..43f688b3 100644 --- a/packages/permissionless/clients/decorators/passkeyServer.ts +++ b/packages/permissionless/clients/decorators/passkeyServer.ts @@ -4,11 +4,20 @@ import { type GetCredentialsReturnType, getCredentials } from "../../actions/passkeyServer/getCredentials.js" +import { + type StartAuthenticationReturnType, + startAuthentication +} from "../../actions/passkeyServer/startAuthentication.js" import { type StartRegistrationParameters, type StartRegistrationReturnType, startRegistration } from "../../actions/passkeyServer/startRegistration.js" +import { + type VerifyAuthenticationParameters, + type VerifyAuthenticationReturnType, + verifyAuthentication +} from "../../actions/passkeyServer/verifyAuthentication.js" import { type VerifyRegistrationParameters, type VerifyRegistrationReturnType, @@ -23,6 +32,10 @@ export type PasskeyServerActions = { verifyRegistration: ( args: VerifyRegistrationParameters ) => Promise + startAuthentication: () => Promise + verifyAuthentication: ( + args: VerifyAuthenticationParameters + ) => Promise getCredentials: ( args: GetCredentialsParameters ) => Promise @@ -38,5 +51,7 @@ export const passkeyServerActions = ( ): PasskeyServerActions => ({ startRegistration: (args) => startRegistration(client, args), verifyRegistration: (args) => verifyRegistration(client, args), - getCredentials: (args) => getCredentials(client, args) + getCredentials: (args) => getCredentials(client, args), + startAuthentication: () => startAuthentication(client), + verifyAuthentication: (args) => verifyAuthentication(client, args) }) diff --git a/packages/permissionless/types/passkeyServer.ts b/packages/permissionless/types/passkeyServer.ts index d2b8c45d..0f94c496 100644 --- a/packages/permissionless/types/passkeyServer.ts +++ b/packages/permissionless/types/passkeyServer.ts @@ -1,6 +1,17 @@ import type { Hex } from "viem" export type PasskeyServerRpcSchema = [ + { + Method: "pks_startAuthentication" + Parameters: [] + ReturnType: { + challenge: string + rpId: string + timeout?: number + userVerification?: "required" | "preferred" | "discouraged" + uuid: string + } + }, { Method: "pks_startRegistration" Parameters: [context: unknown] @@ -71,6 +82,36 @@ export type PasskeyServerRpcSchema = [ publicKey: Hex } }, + { + Method: "pks_verifyAuthentication" + Parameters: [ + { + id: string + rawId: string + response: { + clientDataJSON: string + authenticatorData: string + signature: string + userHandle?: string + } + authenticatorAttachment: "cross-platform" | "platform" + clientExtensionResults: { + appid?: boolean + credProps?: { + rk?: boolean + } + hmacCreateSecret?: boolean + } + type: "public-key" + }, + context: unknown + ] + ReturnType: { + success: boolean + id: string + publicKey: Hex + } + }, { Method: "pks_getCredentials" Parameters: [context: unknown] diff --git a/packages/wagmi-demo/src/PasskeyServerDemo.tsx b/packages/wagmi-demo/src/PasskeyServerDemo.tsx index 2af58584..162a9d68 100644 --- a/packages/wagmi-demo/src/PasskeyServerDemo.tsx +++ b/packages/wagmi-demo/src/PasskeyServerDemo.tsx @@ -1,3 +1,4 @@ +import { WebAuthnP256 } from "ox" import { type SmartAccountClient, createSmartAccountClient @@ -129,10 +130,11 @@ export function PasskeyServerDemo() { }, [credential]) const createCredential = async () => { + const userName = crypto.randomUUID() const credential = await createWebAuthnCredential( await passkeyServerClient.startRegistration({ context: { - userName: "plusminushalf" + userName } }) ) @@ -140,25 +142,26 @@ export function PasskeyServerDemo() { { credential, context: { - userName: "plusminushalf" + userName } } ) - console.log({ verifiedCredential }) - setCredential(verifiedCredential) } const loginCredential = async () => { - const credentials = await passkeyServerClient.getCredentials({ - context: { - userName: "plusminushalf" - } - }) - console.log(credentials) + const credentials = await passkeyServerClient.startAuthentication() + + const response = await WebAuthnP256.sign(credentials) - setCredential(credentials[0]) + const verifiedCredential = + await passkeyServerClient.verifyAuthentication({ + ...response, + uuid: credentials.uuid + }) + + setCredential(verifiedCredential) } const sendUserOperation = async (