# Scenario: Connected App Security E2E

skip: true

**Origin:** Issue #500 security E2E test report
**Date:** 2026-04-11 (initial), 2026-04-12 (regression verified)
**Fixes verified:** PR #751 (hard delete + session revocation), PR #766 (validation skip unchanged), PR #785 (CloudFront all methods), PR #797 (Password leak)

---

## Pre-conditions

| ID | Check | How to verify |
|----|-------|---------------|
| PRE-01 | Admin user (admin@test.com) with system_admin profile | Password login via Advanced Options |
| PRE-02 | At least 2 active real CRM Sales users (not bootstrap/synthetic users such as `sales@test.com`) | Setup > Users: choose users with real Profile rows, e.g. "Aaron Alvarez" |
| PRE-03 | Selected RunAsUsers can actually read Opportunity records | Verify with a temporary Connected App or another impersonation path before running Phases 3/4 |
| PRE-04 | Opportunity records exist | `GET /services/data/v63.0/query?q=SELECT Id FROM Opportunity LIMIT 1` |
| PRE-05 | Staging backend is healthy | `POST /services/oauth2/token` returns JSON (not 503/HTML) |

---

## Phase 1: Connected App Creation

### SEC-CA-01: Create Read-Only Connected App

| Field | Value |
|-------|-------|
| Name | `SEC-E2E-ReadOnly` |
| Scopes | `api.read` only |
| Active | on |

**Steps:**
1. Login as admin > Setup > Connected Apps
2. Click "+ New Connected App"
3. Set Name, select only `api.read` scope
4. **Record ConsumerKey and ConsumerSecret before closing** (secret is shown once)
5. Click Create

**Pass criteria:**
- App appears in list with scope `api.read`, status Active
- ConsumerKey and ConsumerSecret captured

### SEC-CA-02: Create Full-Access Connected App

Same as SEC-CA-01 but:
- Name: `SEC-E2E-FullAccess`
- Scopes: `api`

---

## Phase 2: OAuth Policies Configuration

### SEC-CA-03: Set RunAsUser and verify persistence

**Steps:**
1. Connected Apps list > click gear icon on `SEC-E2E-ReadOnly`
2. In "Run As User" field, search for a real CRM Sales user with Opportunity visibility (e.g., "Aaron Alvarez")
3. Select the user > "Save Policies" button should appear
4. Click "Save Policies" > confirm success toast
5. Click "Back to Connected Apps" > click gear icon again

**Pass criteria:**
- "Save Policies" button appears after selecting user (dirty tracking works)
- After navigation away and back, "Run As User" still shows the selected user name
- If empty after return > **FAIL: Save did not persist**

### SEC-CA-04: Set different RunAsUser on FullAccess app

Same as SEC-CA-03 but for `SEC-E2E-FullAccess` with a different real CRM Sales user.

---

## Phase 3: API — Read-Only App

### SEC-CA-05: Token acquisition (ReadOnly)

```bash
curl -s -D- -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$RO_KEY" \
  -d "client_secret=$RO_SECRET"
```

**Pass criteria:**
- HTTP 200 with `access_token` field
- Response header has NO `X-Deprecation-Warning` (RunAsUserId was correctly used)
- JWT payload shows correct user name, profile "Sales", scope "api.read"

### SEC-CA-06: Read succeeds (ReadOnly)

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/services/data/v63.0/query?q=SELECT+Id,Name,StageName,Amount+FROM+Opportunity+LIMIT+5"
```

**Pass criteria:** HTTP 200 with Opportunity records in JSON

### SEC-CA-07: POST blocked by scope (ReadOnly)

```bash
curl -s -w "\nHTTP: %{http_code}\nContent-Type: %{content_type}" -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$BASE/services/data/v63.0/sobjects/Opportunity" \
  -d '{"Name":"SEC-WriteBlock","StageName":"Prospecting","CloseDate":"2026-12-31"}'
```

**Pass criteria:**
- HTTP 403
- Body: `{"errorCode":"INSUFFICIENT_SCOPE",...}`
- Content-Type: `application/json` (NOT `text/html` — if HTML, request hit CDN not backend)

### SEC-CA-08: PATCH blocked by scope (ReadOnly)

```bash
curl -s -w "\nHTTP: %{http_code}" -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$BASE/services/data/v63.0/sobjects/Opportunity/$ANY_ID" \
  -d '{"Name":"SEC-PatchBlock"}'
```

**Pass criteria:** HTTP 403 `INSUFFICIENT_SCOPE`

### SEC-CA-09: DELETE blocked by scope (ReadOnly)

```bash
curl -s -w "\nHTTP: %{http_code}" -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/services/data/v63.0/sobjects/Opportunity/$ANY_ID"
```

**Pass criteria:** HTTP 403 `INSUFFICIENT_SCOPE`

---

## Phase 4: API — Full-Access App

### SEC-CA-10: Token acquisition (FullAccess)

Same as SEC-CA-05 but with FullAccess credentials. Scope in JWT should be "api".

### SEC-CA-11: Read succeeds (FullAccess)

Same as SEC-CA-06. Additionally verify: different RunAsUser sees different data (RLS is effective).

### SEC-CA-12: Write succeeds (FullAccess)

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$BASE/services/data/v63.0/sobjects/Opportunity" \
  -d '{"Name":"SEC-E2E-WriteTest","StageName":"Prospecting","CloseDate":"2026-12-31"}'
```

**Pass criteria:** HTTP 201 Created. Record ID in response.

### SEC-CA-13: Clean up test record

```bash
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  "$BASE/services/data/v63.0/sobjects/Opportunity/$CREATED_ID"
```

**Pass criteria:** HTTP 200 or 204 if the selected RunAsUser has Opportunity delete permission. Otherwise, perform cleanup as admin and record that delete authorization was not part of this Connected App scope check.

---

## Phase 5: User Deactivation Lifecycle

### SEC-CA-14: Deactivate RunAsUser

**Steps:**
1. Get a fresh ReadOnly token (SEC-CA-05), confirm read works
2. Setup > Users > find RunAsUser > "..." > Deactivate User

**Pass criteria:**
- Status changes to Inactive (no console errors — previously failed with Password validation)
- Zero `validation error on field 'Password': is required` errors in console

### SEC-CA-15: New token rejected after user deactivation

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** HTTP 400 or 401 (RunAsUser is inactive, `rejectInactiveUser()` blocks)

### SEC-CA-16: Old token rejected after user deactivation

```bash
curl -s -H "Authorization: Bearer $OLD_TOKEN" \
  "$BASE/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` (sessions revoked on user deactivation; up to 60s cache grace period)

### SEC-CA-17: Reactivate user and verify recovery

**Steps:**
1. Setup > Users > find RunAsUser > "..." > Activate User
2. Get new token (SEC-CA-05)
3. Read query

**Pass criteria:** Token issued, read returns data.

---

## Phase 6: App Deactivation Lifecycle

### SEC-CA-18: Deactivate ReadOnly app

**Steps:**
1. Connected Apps list > click Zap icon on `SEC-E2E-ReadOnly`
2. Status changes to Inactive

### SEC-CA-19: New token rejected after app deactivation

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** HTTP 400 `invalid_client` (app disabled)

### SEC-CA-20: Old token rejected after app deactivation

```bash
curl -s -H "Authorization: Bearer $OLD_TOKEN" \
  "$BASE/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` (sessions revoked on deactivation)

### SEC-CA-21: Reactivate and verify recovery

Click Zap icon again > Active. Get new token > read succeeds.

---

## Phase 7: App Deletion Lifecycle

### SEC-CA-22: Delete FullAccess app

**Steps:**
1. Get a fresh FullAccess token, confirm read works
2. Connected Apps list > trash icon on `SEC-E2E-FullAccess` > confirm

**Pass criteria:** App removed from list

### SEC-CA-23: New token rejected after deletion

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=$FA_KEY&client_secret=$FA_SECRET"
```

**Pass criteria:** HTTP 400 `invalid_client` / `unknown client_id` (hard delete, row gone)

### SEC-CA-24: Old token rejected after deletion

```bash
curl -s -H "Authorization: Bearer $OLD_FA_TOKEN" \
  "$BASE/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` (sessions revoked in same transaction as delete)

### SEC-CA-25: Verify hard delete (Anti-Security-Theater check)

Re-run SEC-CA-23 to confirm the app is physically gone, not just soft-deleted.

**Pass criteria:** `unknown client_id` (not "connected app is disabled" — that would indicate soft-delete)

---

## Phase 8: Edge Cases

### SEC-CA-26: Wrong ConsumerSecret

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=WRONG_SECRET"
```

**Pass criteria:** HTTP 400 `invalid_client`

### SEC-CA-27: Non-existent ConsumerKey

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=nonexistent-key&client_secret=anything"
```

**Pass criteria:** HTTP 400 `invalid_client`

### SEC-CA-28: Missing parameters

```bash
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials"
```

**Pass criteria:** HTTP 400 `invalid_request` (client_id and client_secret are required)

### SEC-CA-29: Scope escalation attempt

```bash
# ReadOnly app (api.read) but request "api" scope
curl -s -X POST $BASE/services/oauth2/token \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET&scope=api"
```

**Pass criteria:** Token issued but scope in JWT is `api.read` (downgraded, not escalated)

### SEC-CA-30: Invalid bearer token

```bash
curl -s -H "Authorization: Bearer INVALID_TOKEN_STRING" \
  "$BASE/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401

---

## Phase 9: Cleanup

| Action | Verify |
|--------|--------|
| Delete `SEC-E2E-ReadOnly` if still exists | Removed from list |
| Delete `SEC-E2E-FullAccess` if still exists | Removed from list |
| Reactivate RunAsUser if deactivated | Status = Active |
| Delete any test Opportunity records | Not in recycle bin |

---

## Summary Checklist

| # | Test | Security Level | Expected |
|---|------|---------------|----------|
| 03 | RunAsUser persistence | P0 | Persisted after nav-away |
| 05 | ReadOnly token (no deprecation warning) | P0 | 200, correct user/scope |
| 06 | ReadOnly read | P0 | 200 + data |
| 07 | ReadOnly POST blocked | P0-Critical | 403 INSUFFICIENT_SCOPE |
| 08 | ReadOnly PATCH blocked | P0-Critical | 403 INSUFFICIENT_SCOPE |
| 09 | ReadOnly DELETE blocked | P0-Critical | 403 INSUFFICIENT_SCOPE |
| 12 | FullAccess write | P1 | 201 Created |
| 14 | User deactivate (no Password error) | P1 | Inactive, 0 console errors |
| 15 | Deactivated user new token rejected | P0 | 400/401 |
| 16 | Deactivated user old token rejected | P0 | 401 (within 60s) |
| 19 | Deactivated app new token rejected | P0 | 400 |
| 20 | Deactivated app old token revoked | P0 | 401 |
| 23 | Deleted app new token rejected | P0 | 400 unknown client_id |
| 24 | Deleted app old token revoked | P0 | 401 |
| 26 | Wrong secret | P1 | 400 |
| 29 | Scope escalation blocked | P1 | Downgraded to api.read |
| 30 | Invalid token | P1 | 401 |

**All 30 tests must PASS for release approval.**
