Registers a passkey for an employee who holds a recovery enrollment token instead of a session
- the path used when someone has lost every passkey and cannot log in at all. Both endpoints are
unauthenticated (no Authorization header); the token itself stands in for the session.
See webauthn for how an admin issues the token (POST /employee/:id/webauthn/enroll,
documented on webauthn) and the recovery procedure end to end.
EmployeeWebauthnEnrollTokens keyed on the
employee's own ''_id'' - one pending token per employee. * Valid for **one hour**. This is enforced in application code (a ''created > cutoff'' comparison) independently of the collection's TTL index, so an expired token cannot be used even if ''create-webauthn-indexes.js'' was never run to create that index, or MongoDB's once-a-minute TTL sweep has not yet run. * **Single-use**, consumed atomically by ''POST /auth/webauthn/enroll'' via ''findOneAndDelete'' - two concurrent requests racing the same token can never both succeed. * The token is consumed **before** the challenge is checked and before the WebAuthn response is verified. This is deliberate: if verification then fails, the token stays spent rather than reusable, which is the correct trade-off for a single-use recovery credential - but it does mean a failed enroll attempt (wrong relying party, cancelled prompt, expired challenge) burns the token, and the admin must issue a fresh one. * An unknown or expired token also registers a brute-force attempt against the caller's IP. * Both endpoints are rate-limited on the ''login'' scope, keyed by a hash of the token itself (not the caller's IP). * Both endpoints re-check that the employee and their customer may still log in (state ''ENABLED'', access level not ''NO_LOGIN'', not ''m2mOnly'', customer ''ACTIVE'') - the same gate a password or passkey login applies.
Leaves the token in place (does not consume it), so the same token can still be used by
POST /auth/webauthn/enroll afterwards.
| URL | https://api.telecomx.dk/auth/webauthn/enroll/options | ||
|---|---|---|---|
| Method | POST | ||
| Header | Origin | String | Required. Decides which relying party the new passkey is created for |
| Body | JSON object | ||
| token | String | The enrollment token, exactly 43 characters | |
| Property | Type | Description |
|---|---|---|
| challengeId | String | Pass unchanged to the enroll step below |
| options | Object | PublicKeyCredentialCreationOptions, built the same way as webauthn-register-options - residentKey: “required”, userVerification: “required”, attestationType: “none”, excludeCredentials listing the employee's existing credentials for this relying party |
| Error code | Message | Description |
|---|---|---|
| 401 | origin_invalid | Origin header is missing, not https (outside development), or not a known/active portal |
| 401 | enroll_token_invalid | Token is unknown, expired, or the account/customer may not log in |
| 401 | account_disabled | Employee state is not ENABLED, or access level is NO_LOGIN, or the customer is not ACTIVE |
| 401 | m2m_only | The employee is m2mOnly |
| 404 | customer | The employee's customer document could not be found |
| 429 | (empty) | Rate limit exceeded on the login scope, keyed by the token |
Consumes the token, verifies the response, and stores the credential. Sends the employee the same “a passkey was added” notification e-mail as webauthn-register.
| URL | https://api.telecomx.dk/auth/webauthn/enroll | ||
|---|---|---|---|
| Method | POST | ||
| Header | Origin | String | Required. Must be the same origin the challenge was minted for |
| Body | JSON object | ||
| token | String | The enrollment token, exactly 43 characters | |
| challengeId | String | The id returned by the enroll/options call above, exactly 43 characters | |
| response | Object | The RegistrationResponseJSON from the browser |
|
| name | String | Optional, 1-60 characters. Defaults the same way as webauthn-register | |
{ success: true }
Unlike webauthn-register, this does not return the stored credential in the response body.
| Error code | Message | Description |
|---|---|---|
| 401 | origin_invalid | Origin header is missing, not https (outside development), or not a known/active portal |
| 401 | enroll_token_invalid | Token is unknown, already consumed, expired, or the account/customer may not log in |
| 401 | account_disabled | Employee state is not ENABLED, or access level is NO_LOGIN, or the customer is not ACTIVE |
| 401 | m2m_only | The employee is m2mOnly |
| 404 | customer | The employee's customer document could not be found |
| 401 | challenge_invalid | Challenge is malformed, expired, already used, or its stored rpId/origin/employee does not match this request |
| 401 | verification_failed | The attestation could not be verified |
| 409 | credential | This credential id is already registered - to anyone |
| 429 | (empty) | Rate limit exceeded on the login scope, keyed by the token |