Skip to content

[Request]: Add userVerification option to createZksyncPasskeyClient for WebAuthn compatibility #234

@JackHamer09

Description

@JackHamer09

📝 Description

Description

The zksync-sso library does not support passing the userVerification parameter during passkey authentication, which causes authentication failures when using certain passkey providers (notably Google Password Manager) that require explicit user verification.

Problem

When calling passkeyClient.createSession(), users are prompted to select a passkey. However, after selecting the passkey, the transaction fails with the following error:

Payment error: TransactionExecutionError: An error occurred.

Request Arguments:
  from:  0xe9846123569718E020bF55C92CC53fBa414A6719
  to:    0x64Fa4b6fCF655024e6d540E0dFcA4142107D4fBC
  data:  0x5a0694d2...
  gas:   10000000

Details: User verification required, but user could not be verified
Version: viem@2.35.1

Root Cause Confirmed: The issue is caused by the missing userVerification property in the WebAuthn authentication request. Google Password Manager (and potentially other passkey providers) do not prompt for biometric/PIN verification unless the userVerification property is explicitly set to "required" in the WebAuthn request options.

Code Context

PasskeyClient Creation

const passkeyClient = createZksyncPasskeyClient({
  address: getAddress(active?.address ?? ''),
  credentialPublicKey: Uint8Array.from(
    Buffer.from(credentialPublicKeys[active?.address ?? ''], 'base64')
  ),
  userDisplayName: response.data.email,
  userName: response.data.email,
  contracts: {
    session: '0x64Fa4b6fCF655024e6d540E0dFcA4142107D4fBC',
    recovery: '0x6AA83E35439D71F28273Df396BC7768dbaA9849D',
    passkey: '0x006ecc2D79242F1986b7cb5F636d6E3f499f1026',
    accountFactory: '0x7230ae6D4a2C367ff8493a76c15F8832c62f9fE9',
    recoveryOidc: '0x116A07f88d03bD3982eBD5f2667EB08965aAe98c',
    oidcKeyRegistry: '0x0EEeA31EA37959316dc6b50307BaF09528d3fcc4'
  } as any,
  chain: zksyncSepoliaTestnet as any,
  transport: http() as any
});

Session Configuration and Creation

const sessionConfig = {
  signer: sessionPublicKey,
  expiresAt: BigInt(Math.floor(Date.now() / 1000) + 60 * 5), // 5 minutes
  feeLimit: {
    limitType: LimitType.Lifetime as any,
    limit: parseEther('0.1'),
    period: 0n
  },
  transferPolicies: transferPolicies as any,
  callPolicies: []
};

await passkeyClient.createSession({
  sessionConfig
});

Where transferPolicies contains payment targets and limits:

const transferPolicies = shopPayments.map((payment) => ({
  target: getAddress(payment.walletAddress),
  maxValuePerUse: parseEther(payment.amount.eth.toString()),
  valueLimit: {
    limitType: LimitType.Lifetime as any,
    limit: parseEther(payment.amount.eth.toString()),
    period: 0n
  }
}));

Current Implementation

The requestPasskeyAuthentication function in zksync-sso/client/passkey/actions/passkey.ts does not accept or pass the userVerification parameter:

export const requestPasskeyAuthentication = async (
  args: RequestPasskeyAuthenticationArgs
): Promise<RequestPasskeyAuthenticationReturnType> => {
  const passkeyAuthenticationOptions = await generatePasskeyAuthenticationOptions({
    challenge: toBytes(args.challenge),
  });
  // ... rest of the function
};

Proposed Solution

Add an optional userVerification parameter to createZksyncPasskeyClient that gets passed through to all authentication requests:

  1. Update createZksyncPasskeyClient parameters:
const passkeyClient = createZksyncPasskeyClient({
  address: getAddress(active?.address ?? ''),
  credentialPublicKey: Uint8Array.from(...),
  userDisplayName: response.data.email,
  userName: response.data.email,
  userVerification: "required", // <-- Add this option
  contracts: { ... },
  chain: zksyncSepoliaTestnet as any,
  transport: http() as any
});
  1. Update requestPasskeyAuthentication to accept and use the parameter:
export const requestPasskeyAuthentication = async (
  args: RequestPasskeyAuthenticationArgs
): Promise<RequestPasskeyAuthenticationReturnType> => {
  const passkeyAuthenticationOptions = await generatePasskeyAuthenticationOptions({
    challenge: toBytes(args.challenge),
    userVerification: args.userVerification || "preferred",
  });
  // ... rest of the function
};

This way, the userVerification setting is configured once at the client level and automatically applied to all authentication requests.

Steps to Reproduce

  1. Create a passkey client using createZksyncPasskeyClient with the configuration shown above
  2. Create a session configuration with transfer policies and fee limits
  3. Call passkeyClient.createSession({ sessionConfig })
  4. Select a passkey from Google Password Manager when prompted
  5. Observe the error: "User verification required, but user could not be verified"

Expected Behavior

The passkey authentication should prompt for biometric/PIN verification and successfully complete the session creation, allowing the subsequent transactions to execute.

Actual Behavior

The authentication fails without prompting for user verification, causing the transaction to fail with a TransactionExecutionError.

Environment

  • Library: zksync-sso@0.3.3
  • viem Version: 2.35.1
  • Chain: zkSync Sepolia Testnet
  • Passkey Provider: Google Password Manager
  • Browser: Chrome/Edge (Chromium-based)
  • Related Libraries: @simplewebauthn/browser@13.1.0, @simplewebauthn/server@13.1.1

Additional Context

This is a WebAuthn specification requirement. According to the WebAuthn spec, the userVerification parameter controls whether the authenticator should verify the user (via PIN, biometric, etc.). Many passkey providers, especially Google Password Manager, rely on this flag being explicitly set to trigger the verification flow.

The issue occurs specifically during the createSession call, which internally uses requestPasskeyAuthentication to sign the session creation transaction. Without the userVerification flag, the passkey is selected but the verification step is skipped, leading to the error.


🤔 Rationale

No response

📋 Additional context

No response

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions