`POST /auth/webauthn/finish` — answer the challenge with the browser's assertion and, on success, issue the session exactly as `/auth/session` does ([`establish_session`]) minus the MFA gate: a user-verified WebAuthn sign-in is the strong factor.
/auth/webauthn/finishAll four of /auth/session's login-CSRF controls apply unchanged — Json
only, no CORS layer, the Sec-Fetch-Site CSRF layer and __Host- cookies —
and the tokens are minted server-side, so a caller cannot choose them.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
POST /auth/webauthn/finish's body. No Debug: session is a live
challenge handle.
Response Body
application/json
curl -X POST "https://example.com/auth/webauthn/finish" \ -H "Content-Type: application/json" \ -d '{ "credential": "string", "email": "string", "session": "string", "slug": "string" }'{ "organization_slug": "string", "user": { "avatar": "string", "created_at": "string", "email": "string", "id": "string", "is_admin": true, "kind": "native", "last_login_at": "string", "name": "string", "org_id": "string", "provisioned_by": "string", "role": null, "status": null, "status_unrecognized": "string", "team_ids": [ "string" ], "updated_at": "string" }}`POST /auth/session` — exchange browser-minted Cognito tokens for the session cookies. The **only** route that issues them. POST
# Verification is the whole endpoint [`authenticate_bearer`] runs signature, `iss`, `exp`, `aud`, `token_use`, the closed trusted-issuer set, the org-header match and the #317 session-revocation check, with `require_cognito` — you exchange an identity-provider session for our cookies, never a self-issued OAuth token. Without it this endpoint would set a session cookie from an attacker-chosen string. `require_acting_team` is `false`: the client learns its teams *from this response*, so demanding an acting team to sign in is unsatisfiable. Same arm `GET /users/me` takes. # ⚠️ Login CSRF / session fixation, and the four things that stop it The attack is forcing a victim's browser to POST an attacker's tokens here, so the victim then works inside the attacker's session — for a log product, the victim's queries landing in the attacker's org with the audit trail naming the attacker's user. Four controls block it, and the first two are the ones a refactor could quietly remove: 1. **`Json` refuses a cross-site form post.** An HTML `<form>` can only send `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`; axum's `Json` extractor answers 415 to all three. Accepting form encoding here — or hand-parsing the body — deletes this defence. `a_form_encodable_post_is_refused` is the guard; 2. **the API has no CORS layer**, so a cross-origin `fetch(..., {credentials: 'include'})` fails CORS and the browser discards this response's `Set-Cookie` (rh-jculnc); 3. **the CSRF layer** refuses this route like any other — `rh-cl95c`'s predicate keys on `Sec-Fetch-Site`, not on a session cookie, precisely so it covers the route that *creates* the session (a cookie-gated check could not, by construction); 4. **`__Host-`** means only script on the victim's own origin could plant a cookie directly, which is already game over for other reasons.
`POST /auth/webauthn/start` — begin a passkey / security-key sign-in (`rh-kk6vbu`). Anonymous; the server runs Cognito's `USER_AUTH` flow with `PREFERRED_CHALLENGE=WEB_AUTHN` on the org's app client. POST
Every "no challenge" outcome — no key registered, unknown user, unknown org — is one 400 with one sentence, so this is no better an account oracle than the pool's `preventUserExistenceErrors` already allows. `Json` only, like `/auth/session` and for the same login-CSRF reason: a form post is a 415 before the handler runs.