---
id: w4/m16/t003
title: "Hardening: challenge-bound CSRF, subject↔session binding, no open redirect, deny-by-default, login-first when unauthenticated"
worker: worker4
status: done
estimate: 30m
depends_on: [w4/m16/t002]
---

## Objective

Make the consent decision abuse-resistant. The approve action mints a credential for a third party, so it must be impossible to trigger cross-site or on behalf of another user: bind the decision to the challenge and the session, keep every failure path a deny, and never redirect anywhere Hydra didn't supply.

## Context

- **Subject↔session binding:** Hydra's consent request carries `subject` (the Kratos identity id that passed login). The approve/deny server functions must fetch the current Kratos session (`dashboard/src/common/server-fn/session.ts` / `toSession`) and refuse when `consent.subject !== session.identity.id` — otherwise a challenge minted for one user could be approved from another user's browser session.
- **CSRF:** the approve action must not be reachable by a cross-site form POST. TanStack Start server functions + the session check give partial cover; verify what the framework enforces (Origin/Sec-Fetch-Site) and add an explicit check if it's not airtight. The `consent_challenge` is single-use/short-TTL, which bounds replay but does not by itself stop CSRF.
- **No open redirect:** only ever `Response.redirect` to `redirect_to` values returned by the Hydra admin API — never to caller-supplied URLs.
- **Login-first:** a consent challenge hitting the route with no Kratos session should redirect to `/auth/login` with a `return_to` back to the consent URL (Kratos-side `return_to` allowlist permitting), not render or deny.
- **Deny-by-default:** any lookup/session/binding failure ⇒ no accept call. The existing stale-challenge → home degrade stays.

## Steps

1. Add the subject↔session check to the approve/deny server functions; add the no-session → login-first redirect to the route loader.
2. Audit/add CSRF protection on the approve action (Origin/Sec-Fetch-Site or an explicit token); document in code why it is sufficient.
3. Grep the diff for any redirect built from request-supplied input; assert only Hydra-returned `redirect_to` values are used.
4. Confirm the deny-by-default invariant on every early-return path.

## Files

- `dashboard/src/common/server-fn/hydra-consent.ts`
- `dashboard/src/routes/auth.consent.tsx`
- `dashboard/src/common/server-fn/session.ts` (read-only reuse)

## Acceptance criteria

- [ ] Approve with a session whose identity ≠ the challenge's subject is refused (no Hydra accept call).
- [ ] A cross-site POST of the approve action is rejected (test simulating a foreign Origin).
- [ ] No session ⇒ redirect to login with return-to-consent; after login the consent page renders.
- [ ] Every error path denies (no accept); no redirect target is caller-controlled.
- [ ] `yarn typecheck` + `yarn lint` clean.

## Out of scope

- Rate-limiting consent attempts (Hydra challenge TTL + single-use is the accepted bound).
