Topolo Docs Topolo Platform
Open workspace

Begin passkey registration

Create an exact-user pending WebAuthn ceremony; browser/authenticator interaction and later sign-in require separate proof.

Outcome

Create an exact-user pending WebAuthn ceremony; browser/authenticator interaction and later sign-in require separate proof.

These examples prove local handler behavior with real isolated WebAuthn/core-session SQL and explicitly synthetic identity/key dependencies.

Before you start

  • Verify pinned identity, the published users:write action permission and exact target management authority. Cookie identity precedes Bearer identity; a handler-local self success is not proof of live gateway permission.

  • Use the intended private browser/authenticator registration ceremony for these exact returned options. CLI/MCP can obtain and submit public ceremony JSON but cannot manufacture human presence or user verification. Never copy a synthetic fixture credential into a real enrollment.

  • Retain the exact target, options and challengeId across begin/complete. Each challenge expires after 300 seconds and verification consumes it before crypto/storage; a failed or unknown completion requires reading current passkeys before separately approving a fresh ceremony.

Discover the target

topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.user_passkeys.registration.begin --detail --json

Resolve the exact authorized user, read user_passkeys.list for that target, and inspect the returned registration options/challengeId. Keep internal stored credential ids separate from public WebAuthn credential ids. Use the intended user/browser authenticator response for those exact options.

Input fields

FieldRequiredMeaning
userIdYesExisting exact user from authorized Auth discovery. The handler allows self sub/userId or target-organization management authority; the published action permission and gateway authorization still apply. Cookie identity precedes Bearer identity. Begin and complete must retain this same target.

Effects

  • Persists a fresh 300-second exact-user registration challenge with its server-derived origin and RP.

  • Atomically cleans expired/old consumed challenge records; does not enroll a credential or prove browser user verification.

Interpret the response

Returns the actual target userId, standards-based browser registration options and a separate pending challengeId. options.user.id encodes the user identifier and is not a new Auth user. timeout is 60000 milliseconds for the browser; the stored challenge expires after 300 seconds. Challenge creation atomically cleans expired/old consumed records and inserts the pending ceremony.

The server derives origin from the request Origin header or request URL and derives the RP from a supported canonical suffix. Development uses topolo.dev, staging stg.topolo.us and production topolo.app. A credential for one RP cannot be assumed usable on another. Options require user verification, prefer resident keys, use none attestation and support ES256/RS256. Existing credential ids are excluded; exclusion is not a server-side uniqueness waiver.

Repeated begin creates independent pending challenges, not an idempotent retry. It does not store a credential or prove sign-in. Pass the complete options to the intended private browser/authenticator ceremony. CLI/MCP can obtain options but do not manufacture human presence, user verification or a physical authenticator response. The captured public synthetic attestation proves local verification only.

Worked examples

These complete raw target-handler responses use real isolated WebAuthn cryptographic verification and canonical SQL with synthetic identities and locally constructed public authenticator responses. No private key is captured. These expired fixture challenges and public credentials authorize no live enrollment. CLI/MCP payloads show the declared shape and require current discovery, an approved human browser ceremony and review of the exact unchanged plan. Local handler proof does not establish gateway parity or browser sign-in.

Begin a pending production-domain registration with actual WebAuthn options

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "cAo-YAw3zgFlJzt49ww8HkF3NddioV2c1WtqvjYz288",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "uTRFgK_-KQavbhU3vOu0wlOepw2TSSfk",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Create another challenge before a duplicate WebAuthn credential-id refusal

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "g97U5ZSkFCvsc-borB30iTNKN1z4DRkqqHwivLyjvkg",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "5EmzNkopT9wEz-k_R9XsK27nXEohD4fz",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Existing credential is excluded and repeated begin creates a distinct pending challenge

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "-zfWWWgiiWA5NLhYDiG5ndXkqpQxsBqJdcwfNXANZrc",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "KjuhzDYv_iZQ8AmiAW7pZdi37-knqVZp",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

After a failed ceremony begin a fresh challenge

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "rOOrkm08qXyKS5B_XHIFc10uc_BMc9m5AOl7RZ_QmiA",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "8FMCdM5-s8D8O-25thPsh2lfLqpdZ2zD",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Organization administrator begins exact managed-user registration

Input · payload.json:

{
  "userId": "usr_managed"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_managed"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_managed",
    "options": {
      "challenge": "ikgUGRp9brjTwu0LTtbQ34ANSXLUPE43J9MNqoAcDvY",
      "rp": {
        "name": "Topolo",
        "id": "stg.topolo.us"
      },
      "user": {
        "id": "dXNyX21hbmFnZWQ",
        "name": "managed@example.test",
        "displayName": "Fixture managed"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "BJ3bZu_GroeXq4CXCw-gAfvE2s5s0GP_",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Canonical development-domain registration uses its own relying party

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "4PnAD_8T9ZHsnAapJIsr0qEcQvlpvmdIrR1CjkJn3pc",
      "rp": {
        "name": "Topolo",
        "id": "topolo.dev"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        },
        {
          "id": "bUcNocjgIM07rpWn6N5i5g",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "WlmA_B2xJEvC3xnFslPcaPbyymW6Dh-W",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "YxIMKAfvQov88Jv7fA-NW6ZhFRyFHY7HvQIwin7feRw",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        },
        {
          "id": "bUcNocjgIM07rpWn6N5i5g",
          "type": "public-key"
        },
        {
          "id": "GJCXipj4BwBJ9d1Lo92Qvg",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "9YDRxJ34dtq3Ou-TtmraVy-DESS3NnXj",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Create pending challenge for expiry example

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "3uBa7hm8CyIf-8DxQlWy7-R73yZs7RwezdbuQJf1GnI",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        },
        {
          "id": "bUcNocjgIM07rpWn6N5i5g",
          "type": "public-key"
        },
        {
          "id": "GJCXipj4BwBJ9d1Lo92Qvg",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "hQQ0GtUQmASGyqUdBqzB3ma0sHxUGrlT",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Create pending challenge before an isolated credential-store failure

Input · payload.json:

{
  "userId": "usr_self"
}

CLI:

topolo actions validate app_topolo_auth.user_passkeys.registration.begin --data @payload.json --json
topolo actions plan app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json
topolo actions call app_topolo_auth.user_passkeys.registration.begin --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.user_passkeys.registration.begin",
    "input": {
      "userId": "usr_self"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Passkey registration initiated",
  "data": {
    "userId": "usr_self",
    "options": {
      "challenge": "_JZvvC4AcgXX9U_5lvDNzb6Y34zeu0zu61aiIkDmIVE",
      "rp": {
        "name": "Topolo",
        "id": "topolo.app"
      },
      "user": {
        "id": "dXNyX3NlbGY",
        "name": "self@example.test",
        "displayName": "Fixture self"
      },
      "pubKeyCredParams": [
        {
          "alg": -7,
          "type": "public-key"
        },
        {
          "alg": -257,
          "type": "public-key"
        }
      ],
      "timeout": 60000,
      "attestation": "none",
      "excludeCredentials": [
        {
          "id": "I0Qi9pd8I3nIfAnQ776ObQ",
          "type": "public-key"
        },
        {
          "id": "bUcNocjgIM07rpWn6N5i5g",
          "type": "public-key"
        },
        {
          "id": "GJCXipj4BwBJ9d1Lo92Qvg",
          "type": "public-key"
        }
      ],
      "authenticatorSelection": {
        "residentKey": "preferred",
        "userVerification": "required",
        "requireResidentKey": false
      },
      "extensions": {
        "credProps": true
      },
      "hints": []
    },
    "challengeId": "4Ygu1Bc-2856JRb4diNhv17W7C8a5tcS",
    "instructions": "Use your device's authenticator to register a new passkey"
  },
  "timestamp": "2026-10-02T00:00:00.000Z"
}

Verify the result

  • Require data.userId to match the reviewed target and retain both complete options and its separate challengeId for the intended browser ceremony.

  • Use registration.complete with the public authenticator response for those exact options, then list same-user credentials. Browser human presence and subsequent sign-in need independent proof.

Errors and recovery

Foreign target authority is refused before registration SQL

Captured local refusal · HTTP 401:

{
  "input": {
    "userId": "usr_foreign"
  },
  "response": {
    "success": false,
    "error": {
      "message": "Access denied: You can only setup passkeys for yourself",
      "code": "AUTH_ERROR",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Missing target is not provisioned

Captured local refusal · HTTP 404:

{
  "input": {
    "userId": "usr_missing"
  },
  "response": {
    "success": false,
    "error": {
      "message": "User not found",
      "code": "NOT_FOUND",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Anonymous registration is refused

Captured local refusal · HTTP 401:

{
  "input": {
    "userId": "usr_self"
  },
  "response": {
    "success": false,
    "error": {
      "message": "Access token required",
      "code": "AUTH_ERROR",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Unsupported relying-party suffix is refused

Captured local refusal · HTTP 500:

{
  "input": {
    "userId": "usr_self"
  },
  "response": {
    "success": false,
    "error": {
      "message": "Unsupported WebAuthn relying-party host",
      "code": "SERVER_ERROR",
      "details": null,
      "timestamp": "2026-10-02T00:00:00.000Z"
    }
  }
}

Published recovery guidance

  • state_conflict (HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid.

  • AUTH_ERROR (HTTP 401): Verify pinned identity, cookie/Bearer consistency, the published permission and exact target authority. Respect an authenticated target denial; do not infer OAuth expiry or switch identities automatically.

  • NOT_FOUND (HTTP 404): Resolve an existing authorized target. Registration does not provision missing users.

  • VALIDATION_ERROR (HTTP 400): Read current passkey state. Do not reuse a consumed/expired challenge or fabricate a credential. Begin a separately reviewed fresh browser ceremony after failed attestation; retain its same target, options and challengeId.

  • SERVER_ERROR (HTTP 500): Preserve the sanitized error/request id and read current credentials before reviewing a retry. A storage failure can follow challenge consumption; unsupported RP hosts are refused. No copied public-key row or guessed endpoint repairs a failed ceremony.

Recovery boundaries

No challenge-cancellation action is published here. Discard unused public ceremony material in the intended interface and let the pending record expire; local cleanup is not server cancellation. A new begin creates another challenge and must be separately reviewed, rather than treating its options as a retry token.

Discovery and evidence

This record belongs to app_topolo_auth. Captures exercise real controller target authority, SimpleWebAuthn options and CBOR/COSE attestation verification, global credential-id uniqueness and exact-user one-use Session SQL in separate isolated databases. Identity lookup and authenticator response construction are synthetic; failure injection is explicit and lifecycle notification is unconfigured. No live user, device, credential, notification or OAuth state was changed. Stored local credentials and cryptographic verification do not prove browser enrollment, normal sign-in, live CLI/MCP gateway permission, recipient delivery or production completion.