# Cross-Domain FWD Spawning — Internal OIDC Variant (v2)

> **Scope**: Internal FWD server → spawned FWD client bootstrapping only. User authentication is unchanged. OIDC subset used: JWT for tokens, JWKS for key distribution.
>
> **Key design decision**: **No cookies**. Third-party cookies in cross-origin iframes are blocked by modern browsers. All session state is maintained through WebSocket + postMessage + signed URLs.

---

## 1. What Breaks Today (Cross-Domain)

| # | Interaction | Why it breaks |
|---|-------------|---------------|
| 1 | [getRemoteUri()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#267-297) appends `?token=<opaque>` | Opaque token, no expiry, no audience scoping |
| 2 | Iframe navigates to `http://other-host:port/...` | Iframe becomes cross-origin; postMessage bridge must pre-know client origin |
| 3 | Spawned client's embedded Jetty has no CORS config | REST fetches from cross-origin iframe are blocked |
| 4 | Session cookies from the client | Third-party cookies blocked in cross-origin iframes by modern browsers |
| 5 | Top-level resource URLs (e.g. PDFs for printing) | No cookie = no auth for plain browser GETs in new tabs |
| 6 | Client iframe loading resources from FWD server | Cross-origin `<script src>`, `<img src>` — can't attach auth headers to HTML tag loads |

> [!NOTE]
> The login AJAX POST ([fwd_sdk.js](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/fwd_sdk.js) → FWD server) is **not** affected — the login page is loaded via `srcdoc` (same-origin with parent).

---

## 2. Solution Overview

```mermaid
flowchart TD
    subgraph "Authentication (unchanged)"
        A[Login form / SSO] -->|credentials| B[FWD Server]
    end

    subgraph "Spawn Handover (JWT)"
        B -->|spawn process| C[FWD Client]
        C -->|clientIsReady| B
        B -->|mint JWT| D["JSON {url, jwt}"]
        D -->|iframe navigate| C
        C -->|validate JWT| E[Session established via WebSocket]
    end

    subgraph "Runtime (cookie-free)"
        E -->|WebSocket| F[Normal operation]
        F -->|reconnect needed| G[postMessage → parent → server → new JWT]
        F -->|PDF/print| H[Signed resource URL in new tab]
        F -->|JS/CSS/img from server| I["Option A: Client proxies server resources<br/>Option B: Pre-signed server URLs"]
    end
```

---

## 3. Initial Handover (JWT)

### 3.1 Flow

```mermaid
sequenceDiagram
    participant Login as Login (srcdoc iframe)
    participant Server as FWD Server
    participant Client as Spawned Client
    participant Parent as web_landing.html

    Login->>Server: POST /gui (credentials)
    Server->>Server: Authenticate + spawn
    Client-->>Server: clientIsReady(uri)
    Server->>Server: Mint JWT {iss, aud, exp, uuid}
    Server-->>Login: JSON {url, jwt, storageId}
    Login->>Parent: postMessage({origin: clientOrigin})
    Note over Parent: Pre-register iframeOrigin
    Login->>Client: navigate iframe to url?token=jwt
    Client->>Client: Validate JWT → establish WebSocket
```

### 3.2 JWT Structure

```json
{
  "iss": "https://fwd-server:8443",
  "sub": "<fwd-user-id>",
  "aud": "https://192.168.1.50:9443",
  "exp": "<+60s>",
  "iat": "<now>",
  "jti": "<unique-id>",
  "uuid": "<spawned client UUID>"
}
```

### 3.3 What Changes vs. Current

| Aspect | Current | New |
|--------|---------|-----|
| Token format | Opaque from `ServerKeyStore` | Signed JWT (RS256/EdDSA) |
| Token scope | No audience, no expiry | `aud` = client origin, `exp` = 30–120s |
| Key distribution | Full `ServerKeyStore` via RPC | Public key via [getServerData()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#315-324) RPC (or JWKS endpoint) |
| Post-handover session | Cookie / in-memory | **WebSocket = session, no cookies** |

---

## 4. Session Continuity Without Cookies

### 4.1 Normal Operation

The WebSocket connection **is** the session. No cookies needed while it's alive. This matches current FWD behavior.

### 4.2 Reconnection (WebSocket Drops)

```mermaid
sequenceDiagram
    participant Client as Client iframe (cross-origin)
    participant Parent as web_landing.html (server-origin)
    participant Server as FWD Server

    Note over Client: WebSocket connection lost
    Client->>Parent: postMessage({type: "MSG_RECONNECT_TOKEN", uuid: "..."})
    Parent->>Server: fetch("/api/reconnect-token?uuid=...") (same-origin)
    Server->>Server: Validate session still active, mint fresh JWT
    Server-->>Parent: {jwt: "<new-token>"}
    Parent->>Client: postMessage({type: "MSG_RECONNECT_TOKEN", jwt: "..."})
    Client->>Client: Reconnect WebSocket with token
```

**Why this works**:
- [web_landing.html](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/web_landing.html) is **same-origin** with the FWD server → its `fetch()` is a first-party request, no CORS issues
- The postMessage bridge is **already in place** for parent↔iframe relay
- The spawned client validates the same JWT format as the initial handover
- Only one new message type (`MSG_RECONNECT_TOKEN`) in the bridge

### 4.3 Top-Level Resource Access (PDFs, Print, Downloads)

For resources that must open in a new browser tab (no iframe, no WebSocket):

```mermaid
sequenceDiagram
    participant App as P2J App (in iframe)
    participant Client as Embedded Jetty
    participant Browser as New browser tab

    App->>Client: WebSocket: "need URL for /reports/invoice.pdf"
    Client->>Client: Mint resource-scoped JWT {res: "/reports/invoice.pdf", exp: +60s, jti: "..."}
    Client-->>App: "https://client:port/reports/invoice.pdf?access=<jwt>"
    App->>Browser: window.open(signedUrl)
    Browser->>Client: GET /reports/invoice.pdf?access=<jwt>
    Client->>Client: Validate JWT: signature ✓, expiry ✓, resource path ✓, jti not replayed ✓
    Client-->>Browser: 200 OK (PDF content)
```

**Properties**:
- Token is **resource-scoped** — [res](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebDriverHandler.java#1513-1527) claim must match the requested path
- Token is **single-use** — `jti` tracked in a short-lived in-memory set
- Token is **short-lived** — 5–30 minutes (configurable), long enough for slow connections and large resources
- Same pattern as S3 pre-signed URLs

### 4.4 Loading FWD Server Resources from the Client Iframe

When the spawned client's HTML page needs `<script>`, `<img>`, `<link>` from the FWD server (cross-origin), there are two options. Both are fully specified; the choice is a deployment-time configuration.

#### Option A — Client-Side Reverse Proxy

```mermaid
sequenceDiagram
    participant Browser as Browser (iframe)
    participant Client as Spawned Client (Embedded Jetty)
    participant Server as FWD Server

    Note over Client: At startup, client knows FWD server URL (config override)
    Browser->>Client: GET /server-res/p2j.screen.js
    Client->>Client: Check local cache
    alt Cache miss
        Client->>Server: GET /webres/p2j.screen.js (server-to-server, no CORS)
        Server-->>Client: 200 OK + resource body
        Client->>Client: Store in cache
    end
    Client-->>Browser: 200 OK + resource + Cache-Control headers
```

**Implementation details**:
- The client's HTML emits `<script src="/server-res/p2j.screen.js">` — same-origin to the client
- The embedded Jetty has a **single handler** at `/server-res/*` that:
  1. Maps the path: `/server-res/foo.js` → `<fwd-server-url>/webres/foo.js`
  2. Fetches from the FWD server using a standard HTTP client (server-to-server, no browser involvement)
  3. Caches the response in memory (with `ETag` / `Last-Modified` for revalidation)
  4. Returns the resource to the browser with appropriate `Cache-Control` headers
- FWD server URL is known at spawn time (passed via config override `client:web:server_origin`)
- **Zero CORS complexity** — the browser never makes a cross-origin request
- **Zero token management** — the server-to-server fetch can use internal auth (e.g. the spawned client's UUID)

**Pros**: No CORS on FWD server, no tokens for resources, simplest browser-side model
**Cons**: Memory/CPU overhead on spawned client for caching + proxying; doubled network bandwidth for first load

---

#### Option B — Pre-Signed Server Resource URLs

```mermaid
sequenceDiagram
    participant Client as Spawned Client (Embedded Jetty)
    participant Server as FWD Server
    participant Browser as Browser (iframe)

    Note over Client: At startup or on HTML generation
    Client->>Server: RPC: getSignedResourceUrls(["/webres/p2j.screen.js", "/webres/style.css", ...])
    Server->>Server: For each resource, mint a resource-scoped JWT
    Server-->>Client: [{path, signedUrl}, ...]
    Client->>Client: Emit HTML with signed URLs
    Client-->>Browser: HTML with signed src attributes

    Browser->>Server: GET /webres/p2j.screen.js?access=<jwt>
    Server->>Server: Validate JWT: signature ✓, exp ✓, resource path ✓
    Server-->>Browser: 200 OK + resource + CORS headers + cache headers
```

**Implementation details**:
- The client's HTML emits `<script src="https://fwd-server:8443/webres/p2j.screen.js?access=<jwt>">` — cross-origin
- The **server-side resource JWT** has a longer TTL than the handover JWT (resource tokens can live minutes to hours since they grant read-only access to static content):
  ```json
  {
    "iss": "https://fwd-server:8443",
    "aud": "https://fwd-server:8443",
    "sub": "<client-uuid>",
    "res": "/webres/p2j.screen.js",
    "exp": "<+3600s>",
    "jti": "<unique-id>"
  }
  ```
- The FWD server validates the JWT on each resource request and adds CORS headers:
  ```
  Access-Control-Allow-Origin: <client-origin>
  Access-Control-Allow-Methods: GET
  Access-Control-Allow-Credentials: false
  Cache-Control: public, max-age=3600
  ```
- The `client-origin` is encoded in the JWT (`sub` maps to `client-uuid`, server looks up the client origin)
- Browser caches resources normally via `Cache-Control`; the signed URL stays valid for the TTL

**Pros**: No proxy overhead on spawned client, leverages browser caching natively, FWD server has full control over resource access
**Cons**: CORS headers needed on FWD server, per-resource JWT management, signed URLs in HTML source

---

#### Trade-off Comparison

| | Option A (Proxy) | Option B (Signed URLs) |
|---|---|---|
| CORS on FWD server | None | Yes (scoped to client origin) |
| Token management | None | Per-resource JWTs |
| Caching | Proxy cache on client | Browser cache (native) |
| Network hops | 2 (browser→client→server) | 1 (browser→server direct) |
| Load on spawned client | Proxy + cache memory | None |
| Complexity | Low (one proxy handler) | Medium (JWT signing + CORS) |
| Resource access control | Implicit (only proxy serves) | Explicit (JWT per resource) |

> [!TIP]
> Both options can coexist. A deployment could use **Option A** by default (simplest) and fall back to **Option B** for specific resources that need direct server access or where proxy overhead is unacceptable. This is controlled via configuration.

---

## 5. postMessage Bridge Changes

The existing bridge in [web_landing.html](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/web_landing.html) needs **one new message type**:

| Message Type | Direction | Purpose |
|---|---|---|
| `MSG_RECONNECT_TOKEN` | Client → Parent | Request a fresh JWT for WebSocket reconnection |
| `MSG_RECONNECT_TOKEN` | Parent → Client | Deliver the fresh JWT |

The parent handler:
```
case 'MSG_RECONNECT_TOKEN':
    fetch('/api/reconnect-token?uuid=' + event.data.uuid)
      .then(r => r.json())
      .then(data => iframeWindow.postMessage({
          msgId: event.data.msgId,
          type: 'MSG_RECONNECT_TOKEN',
          jwt: data.jwt
      }, iframeOrigin));
    break;
```

All existing message types (`MSG_SET_KEY_VALUE`, `MSG_READ_CLIPBOARD_VALUE`, etc.) continue to work unchanged — `postMessage` is inherently cross-origin capable.

---

## 6. CORS Configuration Summary

| Component | Option A (Proxy) | Option B (Signed URLs) |
|-----------|-----------------|------------------------|
| **FWD Server** | No CORS needed. Login is same-origin (srcdoc). Reconnect-token fetch is same-origin (from parent). Server resources are proxied server-to-server. | CORS needed on resource endpoints: `Access-Control-Allow-Origin: <client-origin>`, `GET` only. Login and reconnect-token remain same-origin. |
| **Spawned Client** | No CORS needed for resources (same-origin proxy). WebSocket `Origin` header validated server-side. | Same as Option A — no additional CORS. |

> [!NOTE]
> In both options, the initial iframe navigation (`GET /index.html?token=<jwt>`) is a simple GET by the browser — it does not trigger a CORS preflight. The embedded Jetty only needs to validate the JWT, not add CORS headers for this request.

---

## 7. Component Impact

### Server Side

| File | Change |
|------|--------|
| [WebDriverHandler.java](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebDriverHandler.java) | Mint JWT in [spawnWorker()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebDriverHandler.java#775-1008) after spawn succeeds. Include `jwt` in [writeUrlInResponse()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebDriverHandler.java#1562-1600) JSON. Add `/api/reconnect-token` endpoint. |
| [WebClientSpawner.java](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java) | [getRemoteUri()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#267-297): remove opaque `?token=` appending. |
| [ServerKeyStore.java](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/ServerKeyStore.java) | Add JWT signing key pair. Add `getJwtPublicKey()` for RPC distribution. |
| **[NEW] `JwtService.java`** | `mintToken(claims)` + `validateToken(jwt, audience)`. |

### Client Side (Spawned Process)

| File | Change |
|------|--------|
| `TemporaryClientTask.doWork()` in [WebClientSpawner.java](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#L332-L471) | Receive JWT public key via [getServerData()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#315-324). Pass FWD server URL for resource proxy config. |
| **Embedded Jetty** | JWT validation filter on initial load. Signed-URL validation filter for resource GETs. **Option A**: `/server-res/*` reverse proxy handler with cache. **Option B**: update HTML template to emit signed URLs from server. WebSocket `Origin` validation. |

### Browser / JavaScript

| File | Change |
|------|--------|
| [fwd_sdk.js](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/fwd_sdk.js) | Extract `jwt` from response, append to iframe URL. |
| [web_landing.html](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/web_landing.html) | Add `MSG_RECONNECT_TOKEN` handler to postMessage bridge. |
| [p2j.screen.js](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.screen.js) | Request signed URL via WebSocket before opening new tabs. Request reconnect token via postMessage on connection loss. |

### Configuration

| Key | Description | Default |
|-----|-------------|---------|
| `server:jwt:algorithm` | RS256, EdDSA, or HS256 | `RS256` |
| `server:jwt:ttl_seconds` | Handover token TTL | `60` |
| `server:jwt:resource_ttl_seconds` | Resource-scoped token TTL (top-level access) | `600` |
| `client:web:server_origin` | FWD server origin for proxy + validation | *(auto from spawn)* |

---

## 8. Implementation Roadmap

### Phase 1 — JWT Infrastructure
- Add key pair generation to `ServerKeyStore`
- Create `JwtService` with `mintToken()` / `validateToken()`
- Extend [getServerData()](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/WebClientSpawner.java#315-324) RPC to include public key

### Phase 2 — Spawn Handover
- Mint JWT in `WebDriverHandler.spawnWorker()` post-spawn
- Include `jwt` in response JSON; remove opaque `?token=`
- Update [fwd_sdk.js](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/fwd_sdk.js) to append JWT to iframe URL
- Add JWT validation filter to embedded Jetty initial load

### Phase 3 — Cookie-Free Session Support
- Add `MSG_RECONNECT_TOKEN` to postMessage bridge ([web_landing.html](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/web_landing.html))
- Add `/api/reconnect-token` endpoint to FWD server
- Update `p2j.screen.js` to request reconnect token via postMessage on WebSocket loss
- Add signed resource URL generation for top-level access (PDFs)
- Add `jti` replay detection (in-memory set with TTL)

### Phase 4 — Server Resource Access + Hardening
- **Option A**: Add `/server-res/*` reverse proxy handler to embedded Jetty (fetch + cache from FWD server). Update client HTML generation to use `/server-res/` prefix.
- **Option B**: Add `getSignedResourceUrls()` RPC endpoint to FWD server. Add JWT validation + CORS filter on server resource endpoints. Update client HTML generation to emit signed URLs.
- Add configuration to select Option A, Option B, or both
- Add WebSocket `Origin` header validation
- Key rotation support
- Integration testing with genuinely cross-domain setup (different IPs, no reverse proxy)

---

## 9. What Stays Unchanged

- User authentication (login form, SSO, external OIDC)
- OS process spawning via native tool
- [ClientDriver](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/ClientDriver.java#173-350) entry point
- WebSocket protocol between browser and spawned client
- [web_landing.html](file:///home/hc/gcd/p2j_repo/p2jtrunk/src/com/goldencode/p2j/main/web_landing.html) wrapper / iframe structure
- All existing postMessage bridge message types
- Session fork flow (adapted to use JWT, same pattern)

---

## 10. Security Summary

| Threat | Mitigation |
|--------|------------|
| JWT interception via URL | Short TTL (30–120s), single-use `jti`, audience-restricted |
| Token replay to different client | `aud` = specific client origin |
| Token replay same client | `jti` tracking with TTL-based expiry |
| Third-party cookie attacks | **No cookies used anywhere** |
| Cross-origin iframe attacks | postMessage origin validation (existing) |
| WebSocket hijacking | Server-side `Origin` header validation |
| Server resource leakage (Option A) | Proxied through authenticated client (no direct cross-origin access) |
| Server resource leakage (Option B) | Resource-scoped JWT, read-only, client-origin-restricted CORS |
| Signed URL leakage (PDFs) | Resource-scoped + short-lived + single-use |
