Vigilfield Docs
API referenceAuth

`POST /auth/refresh` — renew the access token from the refresh cookie.

POST/auth/refresh

The credential arrives in __Host-vf_refresh, not in the body: page script cannot read an HttpOnly cookie, so it cannot put the token there any more. This handler reads the jar and builds the port's [RefreshInput] from {cookie, body.slug} — the port and refresh_for are untouched, and know nothing about cookies.

⚠️ Only vf_session and vf_expires_at are re-set. Cognito's REFRESH_TOKEN_AUTH returns no new refresh token, so there is nothing to re-emit; the browser keeps the vf_refresh it has and a second renewal works. Re-setting it from RefreshInput.refresh_token would work too, but only by accident — it would write the same value back, and the first time Cognito did rotate, a handler that re-set the old value would pin the session to a dead credential.

A missing cookie is the same uniform 401 as a bad one. #314's anti-enumeration property is that every probe-able failure is indistinguishable, and "you sent no cookie" is exactly such a probe.

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

POST /auth/refresh's wire request (rh-cl95c D5).

The refresh token is gone from the body: it lives in the HttpOnly __Host-vf_refresh cookie, which page script cannot read and therefore cannot put here. The transport reads the cookie and builds [RefreshInput] from {cookie, body.slug}.

⚠️ This type and [RefreshInput] are deliberately separate, and collapsing them back into one is the change to refuse. The cookie is a transport concern; the port speaks (slug, refresh_token) and knows nothing about how the credential arrived. One type for both would put the cookie's shape into the domain and would make the wire contract change every time the transport did.

Response Body

application/json

curl -X POST "https://example.com/auth/refresh" \  -H "Content-Type: application/json" \  -d '{    "slug": "string"  }'
{  "expires_in": 0}

`POST /auth/logout` — expire the session cookies, and revoke the session row when the caller could be authenticated. POST

# ⚠️ It runs for a caller who cannot authenticate, and that is the fix An `HttpOnly` cookie can only be cleared by the server, so a sign-out gated on a live session is a sign-out that fails exactly when it is needed. With `SelfAuthContext` the extractor refused first and the handler never ran, so four cases left the browser holding a **refresh** token after a deliberate sign-out: an expired access cookie, an already-revoked session (the second call of an idempotent logout), an api-key caller, and a token with no `origin_jti`. [`MaybeSelfAuth`] collapses all four into one shape. **The `None` arm has authenticated nobody.** It may only do what is safe for an anonymous caller — clearing cookies is (the browser is discarding its own state), revoking a server-side session is not, and that is why the revoke hangs off `Some`. Not a forced-logout primitive: `rh-cl95c`'s CSRF layer refuses a state-changing request the browser reports as cross-site, and it keys on `Sec-Fetch-Site` rather than on a session cookie precisely so it still fires on the routes that run without one — this being the first of them. (A cookie-gated check would not have: `SameSite=Strict` withholds the cookie on the very cross-site POST being defended against.)

`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.