···11+# MM-149 Human Test Plan: OAuth PKCE Client
22+33+## Prerequisites
44+55+- macOS with Xcode and iOS Simulator installed
66+- Nix dev shell active (`nix develop --impure --accept-flake-config` from workspace root)
77+- `pnpm install` completed in `apps/identity-wallet/`
88+- `cargo tauri ios init` completed (with PATH and sandbox patches applied per CLAUDE.md)
99+- All automated tests passing:
1010+ ```bash
1111+ cargo test -p relay v013_seeds_identity_wallet_oauth_client
1212+ cargo test -p identity-wallet -- --skip oauth_client --skip device_key
1313+ # oauth_client tests require direct socket access:
1414+ cargo test -p identity-wallet oauth_client
1515+ ```
1616+1717+---
1818+1919+## Phase 1: OAuth PAR + Browser Redirect (AC1.2, AC7.1)
2020+2121+| Step | Action | Expected |
2222+|------|--------|----------|
2323+| 1 | Start the relay: `cargo run -p relay` from workspace root. Wait for "listening on 0.0.0.0:8080" log line. | Relay starts successfully on port 8080. |
2424+| 2 | In a separate terminal, launch the app: `cd apps/identity-wallet && cargo tauri ios dev`. Wait for the Simulator to open and the app to load. | iOS Simulator launches, app displays the Welcome screen. |
2525+| 3 | Complete all 10 onboarding steps: Welcome → Claim Code (enter a valid code) → Email → Handle → Password → Loading → DID Ceremony → DID Success → Shamir Backup (copy Share 3). | Each screen advances correctly. DID ceremony completes without error. |
2626+| 4 | On the "Complete" step, tap "Continue". | Screen transitions to the `authenticating` step. A spinner is visible with text "Opening browser for authentication…". |
2727+| 5 | Observe the iOS Simulator. | Safari opens. The URL bar contains `client_id=dev.malpercio.identitywallet` and `request_uri=urn:ietf:params:oauth:request_uri:...`. This is the relay's `/oauth/authorize` endpoint. |
2828+2929+---
3030+3131+## Phase 2: Token Exchange + Session Establishment (AC2.2, AC2.5, AC7.2)
3232+3333+| Step | Action | Expected |
3434+|------|--------|----------|
3535+| 1 | In Safari on the Simulator, complete the authorization consent flow (enter credentials, approve). | Safari processes the authorization and redirects back to the app via the `dev.malpercio.identitywallet:` URL scheme. |
3636+| 2 | Observe the app. | App transitions from `authenticating` to `authenticated`. A checkmark icon is visible with text "Your identity wallet is ready." |
3737+| 3 | In the `cargo tauri ios dev` terminal output, search for: `retrying token exchange with server nonce nonce=...` | The log line is present, confirming the `use_dpop_nonce` retry path was exercised during token exchange (AC2.5). |
3838+| 4 | In the terminal output, confirm there are no `tracing::error` lines after "OAuth flow complete". | No error-level log lines related to Keychain or token storage. |
3939+4040+---
4141+4242+## Phase 3: Token Persistence + Restart (AC4.1, AC4.2, AC7.3)
4343+4444+| Step | Action | Expected |
4545+|------|--------|----------|
4646+| 1 | With the app in `authenticated` state, force-quit it: swipe up from the Simulator app switcher, or press Cmd+Shift+H then swipe up on the app card. | App process terminates. |
4747+| 2 | Relaunch the app: tap the identity-wallet icon in the Simulator, or re-run `cargo tauri ios dev`. | App opens. |
4848+| 3 | Observe which screen appears. | The Welcome screen does NOT appear. The app starts directly at `authenticated` (checkmark, "Your identity wallet is ready."). Tokens were stored in Keychain (AC4.1) and loaded on restart (AC4.2). |
4949+| 4 | If tracing is enabled, check the terminal for `auth_ready` event emission log. | Log line confirms the event was emitted during startup. |
5050+5151+---
5252+5353+## Phase 4: Token Security Verification (AC4.3)
5454+5555+| Step | Action | Expected |
5656+|------|--------|----------|
5757+| 1 | With the app running in `authenticated` state, open Safari on macOS (the host machine, not the Simulator). | Safari opens. |
5858+| 2 | In Safari's menu bar, go to Develop → Simulator → identity-wallet. | Web Inspector opens for the app's WKWebView. |
5959+| 3 | In the Web Inspector Console tab, run: `JSON.stringify(localStorage)` | Output is `"{}"` or does not contain `access_token`, `refresh_token`, or any OAuth credential strings. |
6060+| 4 | Run: `JSON.stringify(sessionStorage)` | Same result: no OAuth tokens present. |
6161+| 5 | In Web Inspector, open the Storage tab. Check IndexedDB and Cookies sections. | No OAuth tokens present in any JavaScript-accessible storage mechanism. |
6262+6363+---
6464+6565+## Phase 5: Auth Failure + Recovery (AC7.4, AC8.1, AC8.2)
6666+6767+| Step | Action | Expected |
6868+|------|--------|----------|
6969+| 1 | Stop the relay process (Ctrl+C in the relay terminal). | Relay stops. |
7070+| 2 | Uninstall the app from the Simulator: long-press the app icon → Remove App → Delete App. | App is removed, clearing all Keychain data for this app. |
7171+| 3 | Relaunch the app: `cargo tauri ios dev`. Complete all 10 onboarding steps. | App reaches the "Complete" step. |
7272+| 4 | Tap "Continue". | App transitions to `authenticating`. The PAR request fails (relay is down). App transitions to `auth_failed`. An X icon is visible with "Authentication Failed" heading and an error code displayed. |
7373+| 5 | Start the relay again: `cargo run -p relay`. Wait for it to be ready. | Relay starts on port 8080. |
7474+| 6 | In the app on the `auth_failed` screen, tap "Try again". | App transitions to `authenticating` (spinner appears). Safari opens with the authorization URL. No stale error state is visible — `authError` is cleared. |
7575+| 7 | Complete the authorization in Safari. | App transitions from `authenticating` to `authenticated`. The retry succeeded. |
7676+| 8 | To test "Start over": repeat steps 1–4 to reach `auth_failed` again. Then tap "Start over". | App transitions to the `welcome` step (first onboarding step). All form fields are reset — no pre-filled data from the previous attempt. |
7777+7878+---
7979+8080+## End-to-End: Full OAuth Lifecycle
8181+8282+**Purpose:** Validates the complete chain from DPoP key generation through authenticated API calls with transparent token refresh.
8383+8484+| Step | Action | Expected |
8585+|------|--------|----------|
8686+| 1 | Start with a fresh install (uninstall app, start relay). Launch app. Complete onboarding through "Continue". | Safari opens with OAuth authorization URL. |
8787+| 2 | Complete authorization in Safari. | App reaches `authenticated`. |
8888+| 3 | Force-quit and relaunch the app. | App starts at `authenticated` (tokens restored from Keychain). |
8989+| 4 | Observe terminal output for authenticated API calls. | Requests include `Authorization: DPoP ...` and `DPoP: ...` headers. No authentication errors. |
9090+| 5 | Wait for the access token to expire or trigger an authenticated request near expiry. | Terminal logs show "access token refreshed" — lazy refresh fired transparently before the request. No user-visible interruption. |
9191+9292+---
9393+9494+## Traceability
9595+9696+| Acceptance Criterion | Automated Test | Manual Step |
9797+|----------------------|----------------|-------------|
9898+| MM-149.AC1.1 | `par_integration_returns_201_with_request_uri` (ignored, requires relay) | — |
9999+| MM-149.AC1.2 | — | Phase 1, Steps 4–5 |
100100+| MM-149.AC1.3 | `v013_seeds_identity_wallet_oauth_client` | — |
101101+| MM-149.AC1.4 | `par_missing_code_challenge_returns_client_error` (ignored, requires relay) | — |
102102+| MM-149.AC2.1 | `handle_deep_link_delivers_code_and_state` | — |
103103+| MM-149.AC2.2 | — | Phase 2, Steps 1–2 |
104104+| MM-149.AC2.3 | `handle_deep_link_csrf_mismatch_returns_state_mismatch_error` | — |
105105+| MM-149.AC2.4 | `handle_deep_link_replay_is_silently_ignored` | — |
106106+| MM-149.AC2.5 | — | Phase 2, Step 3 |
107107+| MM-149.AC3.1 | `dpop_proof_header_has_required_fields` | — |
108108+| MM-149.AC3.2 | `dpop_proof_claims_has_required_fields` | — |
109109+| MM-149.AC3.3 | `dpop_proof_includes_ath_when_supplied` + `compute_ath_matches_sha256_base64url` | — |
110110+| MM-149.AC3.4 | `dpop_proof_includes_nonce_when_supplied` | — |
111111+| MM-149.AC3.5 | `dpop_proof_signature_verifies_against_embedded_jwk` | — |
112112+| MM-149.AC4.1 | — | Phase 2, Step 4 + Phase 3, Steps 1–3 |
113113+| MM-149.AC4.2 | — | Phase 3, Steps 2–4 |
114114+| MM-149.AC4.3 | — | Phase 4, Steps 1–5 |
115115+| MM-149.AC5.1 | `dpop_and_authorization_headers_present_on_get` | — |
116116+| MM-149.AC5.2 | `nonce_retry_sends_exactly_two_requests` | — |
117117+| MM-149.AC5.3 | `empty_access_token_does_not_panic` | — |
118118+| MM-149.AC6.1 | `lazy_refresh_fires_when_expiry_near` | — |
119119+| MM-149.AC6.2 | `refresh_dpop_proof_has_no_ath_claim` | — |
120120+| MM-149.AC6.3 | `refresh_invalid_grant_returns_token_refresh_failed` | — |
121121+| MM-149.AC7.1 | — | Phase 1, Steps 3–5 |
122122+| MM-149.AC7.2 | — | Phase 2, Steps 1–2 |
123123+| MM-149.AC7.3 | — | Phase 3, Steps 1–3 |
124124+| MM-149.AC7.4 | — | Phase 5, Steps 1–4 |
125125+| MM-149.AC8.1 | — | Phase 5, Steps 5–7 |
126126+| MM-149.AC8.2 | — | Phase 5, Step 8 |