Wei-Shaw/sub2api · warning

Passkey sign-in was cancelled

Error message

Passkey sign-in was cancelled

What it means

In frontend/src/api/passkey.ts:116, after navigator.credentials.get({publicKey}) resolves, the code checks the result with `credential instanceof PublicKeyCredential`. A null return (user dismissed the browser's authenticator prompt in some browsers) or an unexpected object type fails the check and throws 'Passkey sign-in was cancelled'. Note that most user cancellations actually surface as a thrown NotAllowedError from credentials.get() itself; this branch catches the rarer null-return case.

Source

Thrown at frontend/src/api/passkey.ts:116

    response: {
      authenticatorData: bufferToBase64URL(response.authenticatorData),
      clientDataJSON: bufferToBase64URL(response.clientDataJSON),
      signature: bufferToBase64URL(response.signature),
      userHandle: bufferToBase64URL(response.userHandle)
    }
  }
}

async function login(proof?: ActionCaptchaRequestProof): Promise<AuthResponse> {
  requirePasskeySupport()
  const { data: begin } = proof
    ? await apiClient.post<CeremonyOptionsResponse>('/auth/passkey/login/begin', proof)
    : await apiClient.post<CeremonyOptionsResponse>('/auth/passkey/login/begin')
  const credential = await navigator.credentials.get({
    publicKey: requestOptionsFromJSON(begin.options.publicKey)
  })
  if (!(credential instanceof PublicKeyCredential)) {
    throw new Error('Passkey sign-in was cancelled')
  }
  const { data } = await apiClient.post<AuthResponse>('/auth/passkey/login/finish', {
    session_token: begin.session_token,
    credential: serializeAssertionCredential(credential)
  })
  return data
}

async function register(name: string, password: string): Promise<PasskeyCredentialSummary> {
  requirePasskeySupport()
  const { data: begin } = await apiClient.post<CeremonyOptionsResponse>(
    '/user/passkeys/register/begin',
    { password }
  )
  const credential = await navigator.credentials.create({
    publicKey: creationOptionsFromJSON(begin.options.publicKey)
  })
  if (!(credential instanceof PublicKeyCredential)) {

View on GitHub (pinned to 073e92d171)

Solutions

  1. Catch this error alongside NotAllowedError in the sign-in handler and show a neutral 'cancelled' message with a retry affordance.
  2. Verify the request options from requestOptionsFromJSON(begin.options.publicKey) include challenge, rpId, and allowCredentials correctly — malformed options can make the browser bail.
  3. In automated tests, mock credentials.get to return a real PublicKeyCredential-shaped object and stub the instanceof check, or refactor to duck-typing.
  4. Consider checking `if (!credential)` instead of instanceof so null and wrong-type are handled distinctly.

Example fix

// before
const credential = await navigator.credentials.get({ publicKey: requestOptionsFromJSON(begin.options.publicKey) })
if (!(credential instanceof PublicKeyCredential)) {
  throw new Error('Passkey sign-in was cancelled')
}

// after
const credential = await navigator.credentials.get({ publicKey: requestOptionsFromJSON(begin.options.publicKey) })
if (!credential || !(credential instanceof PublicKeyCredential)) {
  const err = new Error('Passkey sign-in was cancelled')
  err.name = 'PasskeyCancelled'
  throw err
}
// caller:
try { await login() } catch (e) { if (e.name === 'NotAllowedError' || e.name === 'PasskeyCancelled') return retryUi() ; throw e }
Defensive patterns

Strategy: try-catch

Type guard

function isPublicKeyCredential(c: Credential | null): c is PublicKeyCredential {
  return !!c && c instanceof PublicKeyCredential;
}

Try / catch

try {
  await passkeyLogin();
} catch (e) {
  if (e.name === 'NotAllowedError' || e.message === 'Passkey sign-in was cancelled') {
    showInfo('Sign-in cancelled — try again'); return; // user-driven, not a bug
  }
  throw e;
}

Prevention

When it happens

Trigger: navigator.credentials.get() for a passkey login resolves with null — happens in some browsers/WebViews when the account picker is dismissed, when an RSA/Android-keystore response returns a plain Credential, or when a cross-platform mismatch makes the browser return a non-PublicKeyCredential object. Also triggered in test environments where credentials.get is mocked to return null or {}.

Common situations: Users closing the WebAuthn sheet on Android Chrome; Selenium/Playwright tests with fake credential objects that aren't PublicKeyCredential instances; Safari edge cases where the transaction is cancelled after the platform authenticator UI appears.

Related errors


AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15). Data as JSON: /api/errors/8a5f5bc4b4b33d49. Report an issue: GitHub.