Wei-Shaw/sub2api · warning
Passkey creation was cancelled
Error message
Passkey creation was cancelled
What it means
In frontend/src/api/passkey.ts:135, after navigator.credentials.create({publicKey}) for passkey registration, the code requires the resolved value to be an instanceof PublicKeyCredential. When the browser returns null (ceremony aborted in browsers that resolve rather than reject) or a non-PublicKeyCredential object, the code throws 'Passkey creation was cancelled'. Like sign-in, most explicit user cancellations reject with NotAllowedError from credentials.create() itself.
Source
Thrown at frontend/src/api/passkey.ts:135
}
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)) {
throw new Error('Passkey creation was cancelled')
}
const { data } = await apiClient.post<PasskeyCredentialSummary>(
'/user/passkeys/register/finish',
{
session_token: begin.session_token,
name,
credential: serializeRegistrationCredential(credential)
}
)
return data
}
async function list(): Promise<PasskeyCredentialSummary[]> {
const { data } = await apiClient.get<PasskeyCredentialSummary[]>('/user/passkeys')
return data
}
async function rename(id: number, name: string): Promise<void> {View on GitHub (pinned to 073e92d171)
Solutions
- Treat this error and NotAllowedError identically in the UI: show 'registration cancelled, try again' rather than a generic failure.
- Validate creationOptionsFromJSON(begin.options.publicKey) output includes user.id, user.name, rp.id, challenge and pubKeyCredParams — malformed options cause silent aborts in some browsers.
- In E2E tests, use virtual authenticators (Playwright/Chrome --enable-features or CDP WebAuthn) instead of mocking create().
- Refactor to `if (!credential || !(credential instanceof PublicKeyCredential))` for clearer null handling.
Example fix
// before
const credential = await navigator.credentials.create({ publicKey: creationOptionsFromJSON(begin.options.publicKey) })
if (!(credential instanceof PublicKeyCredential)) {
throw new Error('Passkey creation was cancelled')
}
// after
const credential = await navigator.credentials.create({ publicKey: creationOptionsFromJSON(begin.options.publicKey) })
if (!credential || typeof credential.id === 'undefined') {
throw new Error('Passkey creation was cancelled')
}
// narrow via duck-typing instead of instanceof so mocks/edge browsers still pass
if (credential && !(credential instanceof PublicKeyCredential) && !('rawId' in credential)) {
throw new Error('Passkey creation was cancelled')
} Defensive patterns
Strategy: try-catch
Type guard
function hasRegistrationShape(c: Credential | null): c is PublicKeyCredential {
return !!c && 'rawId' in c && 'response' in c;
} Try / catch
try {
await registerPasskey(name, password);
} catch (e) {
if (e.name === 'NotAllowedError' || e.message === 'Passkey creation was cancelled') {
showInfo('Registration cancelled — try again'); return;
}
throw e;
} Prevention
- Validate creation options include user.id, user.name, rp.id, challenge and pubKeyCredParams before create()
- Surface a retry action on cancellation rather than a dead-end error
- Use Playwright/CDP virtual authenticators in automated tests
When it happens
Trigger: Registering a passkey where credentials.create() resolves null: dismissed Windows Hello / Touch ID sheet in certain browser builds, WebView without a proper WebAuthn UI, or unit tests mocking create() with null/generic objects. Also triggered by a resident-credential request when the authenticator silently fails to create one and the browser resolves empty.
Common situations: E2E tests (Cypress historically cannot drive real WebAuthn); users on Android WebView-based browsers; users cancelling the save-prompt for a passkey; rp.name/user.name fields missing from creation options causing some browsers to abort.
Related errors
- Passkey sign-in was cancelled
- Passkeys are not supported by this browser
- auth.verifyFailed
- HTTP error! status: ${response.status}
- No response body
AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15).
Data as JSON: /api/errors/4e901c4757b0bdcb.
Report an issue: GitHub.