An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add test plan for MM-149 OAuth PKCE client

authored by

Malpercio and committed by
Tangled
edaa6b10 8782f18b

+126
+126
docs/test-plans/2026-03-23-MM-149.md
··· 1 + # MM-149 Human Test Plan: OAuth PKCE Client 2 + 3 + ## Prerequisites 4 + 5 + - macOS with Xcode and iOS Simulator installed 6 + - Nix dev shell active (`nix develop --impure --accept-flake-config` from workspace root) 7 + - `pnpm install` completed in `apps/identity-wallet/` 8 + - `cargo tauri ios init` completed (with PATH and sandbox patches applied per CLAUDE.md) 9 + - All automated tests passing: 10 + ```bash 11 + cargo test -p relay v013_seeds_identity_wallet_oauth_client 12 + cargo test -p identity-wallet -- --skip oauth_client --skip device_key 13 + # oauth_client tests require direct socket access: 14 + cargo test -p identity-wallet oauth_client 15 + ``` 16 + 17 + --- 18 + 19 + ## Phase 1: OAuth PAR + Browser Redirect (AC1.2, AC7.1) 20 + 21 + | Step | Action | Expected | 22 + |------|--------|----------| 23 + | 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. | 24 + | 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. | 25 + | 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. | 26 + | 4 | On the "Complete" step, tap "Continue". | Screen transitions to the `authenticating` step. A spinner is visible with text "Opening browser for authentication…". | 27 + | 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. | 28 + 29 + --- 30 + 31 + ## Phase 2: Token Exchange + Session Establishment (AC2.2, AC2.5, AC7.2) 32 + 33 + | Step | Action | Expected | 34 + |------|--------|----------| 35 + | 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. | 36 + | 2 | Observe the app. | App transitions from `authenticating` to `authenticated`. A checkmark icon is visible with text "Your identity wallet is ready." | 37 + | 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). | 38 + | 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. | 39 + 40 + --- 41 + 42 + ## Phase 3: Token Persistence + Restart (AC4.1, AC4.2, AC7.3) 43 + 44 + | Step | Action | Expected | 45 + |------|--------|----------| 46 + | 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. | 47 + | 2 | Relaunch the app: tap the identity-wallet icon in the Simulator, or re-run `cargo tauri ios dev`. | App opens. | 48 + | 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). | 49 + | 4 | If tracing is enabled, check the terminal for `auth_ready` event emission log. | Log line confirms the event was emitted during startup. | 50 + 51 + --- 52 + 53 + ## Phase 4: Token Security Verification (AC4.3) 54 + 55 + | Step | Action | Expected | 56 + |------|--------|----------| 57 + | 1 | With the app running in `authenticated` state, open Safari on macOS (the host machine, not the Simulator). | Safari opens. | 58 + | 2 | In Safari's menu bar, go to Develop → Simulator → identity-wallet. | Web Inspector opens for the app's WKWebView. | 59 + | 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. | 60 + | 4 | Run: `JSON.stringify(sessionStorage)` | Same result: no OAuth tokens present. | 61 + | 5 | In Web Inspector, open the Storage tab. Check IndexedDB and Cookies sections. | No OAuth tokens present in any JavaScript-accessible storage mechanism. | 62 + 63 + --- 64 + 65 + ## Phase 5: Auth Failure + Recovery (AC7.4, AC8.1, AC8.2) 66 + 67 + | Step | Action | Expected | 68 + |------|--------|----------| 69 + | 1 | Stop the relay process (Ctrl+C in the relay terminal). | Relay stops. | 70 + | 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. | 71 + | 3 | Relaunch the app: `cargo tauri ios dev`. Complete all 10 onboarding steps. | App reaches the "Complete" step. | 72 + | 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. | 73 + | 5 | Start the relay again: `cargo run -p relay`. Wait for it to be ready. | Relay starts on port 8080. | 74 + | 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. | 75 + | 7 | Complete the authorization in Safari. | App transitions from `authenticating` to `authenticated`. The retry succeeded. | 76 + | 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. | 77 + 78 + --- 79 + 80 + ## End-to-End: Full OAuth Lifecycle 81 + 82 + **Purpose:** Validates the complete chain from DPoP key generation through authenticated API calls with transparent token refresh. 83 + 84 + | Step | Action | Expected | 85 + |------|--------|----------| 86 + | 1 | Start with a fresh install (uninstall app, start relay). Launch app. Complete onboarding through "Continue". | Safari opens with OAuth authorization URL. | 87 + | 2 | Complete authorization in Safari. | App reaches `authenticated`. | 88 + | 3 | Force-quit and relaunch the app. | App starts at `authenticated` (tokens restored from Keychain). | 89 + | 4 | Observe terminal output for authenticated API calls. | Requests include `Authorization: DPoP ...` and `DPoP: ...` headers. No authentication errors. | 90 + | 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. | 91 + 92 + --- 93 + 94 + ## Traceability 95 + 96 + | Acceptance Criterion | Automated Test | Manual Step | 97 + |----------------------|----------------|-------------| 98 + | MM-149.AC1.1 | `par_integration_returns_201_with_request_uri` (ignored, requires relay) | — | 99 + | MM-149.AC1.2 | — | Phase 1, Steps 4–5 | 100 + | MM-149.AC1.3 | `v013_seeds_identity_wallet_oauth_client` | — | 101 + | MM-149.AC1.4 | `par_missing_code_challenge_returns_client_error` (ignored, requires relay) | — | 102 + | MM-149.AC2.1 | `handle_deep_link_delivers_code_and_state` | — | 103 + | MM-149.AC2.2 | — | Phase 2, Steps 1–2 | 104 + | MM-149.AC2.3 | `handle_deep_link_csrf_mismatch_returns_state_mismatch_error` | — | 105 + | MM-149.AC2.4 | `handle_deep_link_replay_is_silently_ignored` | — | 106 + | MM-149.AC2.5 | — | Phase 2, Step 3 | 107 + | MM-149.AC3.1 | `dpop_proof_header_has_required_fields` | — | 108 + | MM-149.AC3.2 | `dpop_proof_claims_has_required_fields` | — | 109 + | MM-149.AC3.3 | `dpop_proof_includes_ath_when_supplied` + `compute_ath_matches_sha256_base64url` | — | 110 + | MM-149.AC3.4 | `dpop_proof_includes_nonce_when_supplied` | — | 111 + | MM-149.AC3.5 | `dpop_proof_signature_verifies_against_embedded_jwk` | — | 112 + | MM-149.AC4.1 | — | Phase 2, Step 4 + Phase 3, Steps 1–3 | 113 + | MM-149.AC4.2 | — | Phase 3, Steps 2–4 | 114 + | MM-149.AC4.3 | — | Phase 4, Steps 1–5 | 115 + | MM-149.AC5.1 | `dpop_and_authorization_headers_present_on_get` | — | 116 + | MM-149.AC5.2 | `nonce_retry_sends_exactly_two_requests` | — | 117 + | MM-149.AC5.3 | `empty_access_token_does_not_panic` | — | 118 + | MM-149.AC6.1 | `lazy_refresh_fires_when_expiry_near` | — | 119 + | MM-149.AC6.2 | `refresh_dpop_proof_has_no_ath_claim` | — | 120 + | MM-149.AC6.3 | `refresh_invalid_grant_returns_token_refresh_failed` | — | 121 + | MM-149.AC7.1 | — | Phase 1, Steps 3–5 | 122 + | MM-149.AC7.2 | — | Phase 2, Steps 1–2 | 123 + | MM-149.AC7.3 | — | Phase 3, Steps 1–3 | 124 + | MM-149.AC7.4 | — | Phase 5, Steps 1–4 | 125 + | MM-149.AC8.1 | — | Phase 5, Steps 5–7 | 126 + | MM-149.AC8.2 | — | Phase 5, Step 8 |