---
id: w4/m16/t004
title: "E2E: extend scripts/auth-oauth21-e2e.sh — DCR client → login → consent approve → token works at /mcp; deny path asserts rejection"
worker: worker4
status: done
estimate: 30m
depends_on: [w4/m16/t002]
---

## Objective

Prove the self-serve consent path end-to-end, scripted, exit 0 = pass. Extend `scripts/auth-oauth21-e2e.sh` (the w4/m9 harness that already does DCR → PKCE authorize → Kratos login → headless consent → token → `/mcp`) with an **untrusted** client that walks the consent page: register via DCR without any operator blessing, drive the authorize flow to the consent page, submit Approve (and separately Deny), and assert the outcomes.

## Context

- The existing script exercises the trusted path (client blessed via `skip_consent`/`OAUTH_TRUSTED_CLIENTS`); keep that assertion — the headless path must remain byte-identical.
- The consent page is HTML + a server-function POST; over curl this means parsing the page/loader response for the challenge and replaying the approve action's request shape (same pattern the script already uses for Kratos login forms). If the approve action proves un-curl-able (framework-internal serialization), fall back to a Playwright step and say so in the script header — but try curl first; keep the script zero-dep if possible.
- Deny assertion: the agent's redirect_uri receives `error=access_denied` and the token endpoint refuses the flow's code (or no code is issued).
- Remember assertion: a second authorize for the same client+user inside the remember window completes without hitting the consent page.

## Steps

1. Add an untrusted-client leg to the script: DCR register (no blessing) → authorize → login → expect the consent page (not 403).
2. Approve leg: submit approve → code → token → authenticated `/mcp` call succeeds.
3. Deny leg: fresh flow → deny → `error=access_denied`, no usable token.
4. Remember leg: repeat authorize → no consent page → token.
5. Keep the trusted-client leg green unchanged.

## Files

- `scripts/auth-oauth21-e2e.sh`

## Acceptance criteria

- [ ] Script exit 0 against a local stack covers: untrusted client consent-page approve → working `/mcp` token; deny → `access_denied` + no token; remembered re-authorize skips consent; trusted client still headless.
- [ ] `bash -n` clean; parameterized for prod reuse like the sibling auth scripts (`KRATOS_PUBLIC_URL`-style env overrides).

## Out of scope

- The manual real-browser Claude Code/Cursor connect walkthrough (ADR025's recipe) — docs task t005 notes it; not scriptable here.
