Project

General

Profile

Cross-Domain FWD Spawning - Internal OIDC Variant.md

Hynek Cihlar, 02/19/2026 06:18 AM

Download (18.4 KB)

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

    
3
> **Scope**: Internal FWD server → spawned FWD client bootstrapping only. User authentication is unchanged. OIDC subset used: JWT for tokens, JWKS for key distribution.
4
>
5
> **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.
6

    
7
---
8

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

    
11
| # | Interaction | Why it breaks |
12
|---|-------------|---------------|
13
| 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 |
14
| 2 | Iframe navigates to `http://other-host:port/...` | Iframe becomes cross-origin; postMessage bridge must pre-know client origin |
15
| 3 | Spawned client's embedded Jetty has no CORS config | REST fetches from cross-origin iframe are blocked |
16
| 4 | Session cookies from the client | Third-party cookies blocked in cross-origin iframes by modern browsers |
17
| 5 | Top-level resource URLs (e.g. PDFs for printing) | No cookie = no auth for plain browser GETs in new tabs |
18
| 6 | Client iframe loading resources from FWD server | Cross-origin `<script src>`, `<img src>` — can't attach auth headers to HTML tag loads |
19

    
20
> [!NOTE]
21
> 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).
22

    
23
---
24

    
25
## 2. Solution Overview
26

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

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

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

    
49
---
50

    
51
## 3. Initial Handover (JWT)
52

    
53
### 3.1 Flow
54

    
55
```mermaid
56
sequenceDiagram
57
    participant Login as Login (srcdoc iframe)
58
    participant Server as FWD Server
59
    participant Client as Spawned Client
60
    participant Parent as web_landing.html
61

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

    
73
### 3.2 JWT Structure
74

    
75
```json
76
{
77
  "iss": "https://fwd-server:8443",
78
  "sub": "<fwd-user-id>",
79
  "aud": "https://192.168.1.50:9443",
80
  "exp": "<+60s>",
81
  "iat": "<now>",
82
  "jti": "<unique-id>",
83
  "uuid": "<spawned client UUID>"
84
}
85
```
86

    
87
### 3.3 What Changes vs. Current
88

    
89
| Aspect | Current | New |
90
|--------|---------|-----|
91
| Token format | Opaque from `ServerKeyStore` | Signed JWT (RS256/EdDSA) |
92
| Token scope | No audience, no expiry | `aud` = client origin, `exp` = 30–120s |
93
| 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) |
94
| Post-handover session | Cookie / in-memory | **WebSocket = session, no cookies** |
95

    
96
---
97

    
98
## 4. Session Continuity Without Cookies
99

    
100
### 4.1 Normal Operation
101

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

    
104
### 4.2 Reconnection (WebSocket Drops)
105

    
106
```mermaid
107
sequenceDiagram
108
    participant Client as Client iframe (cross-origin)
109
    participant Parent as web_landing.html (server-origin)
110
    participant Server as FWD Server
111

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

    
121
**Why this works**:
122
- [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
123
- The postMessage bridge is **already in place** for parent↔iframe relay
124
- The spawned client validates the same JWT format as the initial handover
125
- Only one new message type (`MSG_RECONNECT_TOKEN`) in the bridge
126

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

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

    
131
```mermaid
132
sequenceDiagram
133
    participant App as P2J App (in iframe)
134
    participant Client as Embedded Jetty
135
    participant Browser as New browser tab
136

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

    
146
**Properties**:
147
- 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
148
- Token is **single-use** — `jti` tracked in a short-lived in-memory set
149
- Token is **short-lived** — 5–30 minutes (configurable), long enough for slow connections and large resources
150
- Same pattern as S3 pre-signed URLs
151

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

    
154
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.
155

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

    
158
```mermaid
159
sequenceDiagram
160
    participant Browser as Browser (iframe)
161
    participant Client as Spawned Client (Embedded Jetty)
162
    participant Server as FWD Server
163

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

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

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

    
189
---
190

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

    
193
```mermaid
194
sequenceDiagram
195
    participant Client as Spawned Client (Embedded Jetty)
196
    participant Server as FWD Server
197
    participant Browser as Browser (iframe)
198

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

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

    
211
**Implementation details**:
212
- The client's HTML emits `<script src="https://fwd-server:8443/webres/p2j.screen.js?access=<jwt>">` — cross-origin
213
- 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):
214
  ```json
215
  {
216
    "iss": "https://fwd-server:8443",
217
    "aud": "https://fwd-server:8443",
218
    "sub": "<client-uuid>",
219
    "res": "/webres/p2j.screen.js",
220
    "exp": "<+3600s>",
221
    "jti": "<unique-id>"
222
  }
223
  ```
224
- The FWD server validates the JWT on each resource request and adds CORS headers:
225
  ```
226
  Access-Control-Allow-Origin: <client-origin>
227
  Access-Control-Allow-Methods: GET
228
  Access-Control-Allow-Credentials: false
229
  Cache-Control: public, max-age=3600
230
  ```
231
- The `client-origin` is encoded in the JWT (`sub` maps to `client-uuid`, server looks up the client origin)
232
- Browser caches resources normally via `Cache-Control`; the signed URL stays valid for the TTL
233

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

    
237
---
238

    
239
#### Trade-off Comparison
240

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

    
251
> [!TIP]
252
> 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.
253

    
254
---
255

    
256
## 5. postMessage Bridge Changes
257

    
258
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**:
259

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

    
265
The parent handler:
266
```
267
case 'MSG_RECONNECT_TOKEN':
268
    fetch('/api/reconnect-token?uuid=' + event.data.uuid)
269
      .then(r => r.json())
270
      .then(data => iframeWindow.postMessage({
271
          msgId: event.data.msgId,
272
          type: 'MSG_RECONNECT_TOKEN',
273
          jwt: data.jwt
274
      }, iframeOrigin));
275
    break;
276
```
277

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

    
280
---
281

    
282
## 6. CORS Configuration Summary
283

    
284
| Component | Option A (Proxy) | Option B (Signed URLs) |
285
|-----------|-----------------|------------------------|
286
| **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. |
287
| **Spawned Client** | No CORS needed for resources (same-origin proxy). WebSocket `Origin` header validated server-side. | Same as Option A — no additional CORS. |
288

    
289
> [!NOTE]
290
> 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.
291

    
292
---
293

    
294
## 7. Component Impact
295

    
296
### Server Side
297

    
298
| File | Change |
299
|------|--------|
300
| [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. |
301
| [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. |
302
| [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. |
303
| **[NEW] `JwtService.java`** | `mintToken(claims)` + `validateToken(jwt, audience)`. |
304

    
305
### Client Side (Spawned Process)
306

    
307
| File | Change |
308
|------|--------|
309
| `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. |
310
| **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. |
311

    
312
### Browser / JavaScript
313

    
314
| File | Change |
315
|------|--------|
316
| [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. |
317
| [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. |
318
| [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. |
319

    
320
### Configuration
321

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

    
329
---
330

    
331
## 8. Implementation Roadmap
332

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

    
338
### Phase 2 — Spawn Handover
339
- Mint JWT in `WebDriverHandler.spawnWorker()` post-spawn
340
- Include `jwt` in response JSON; remove opaque `?token=`
341
- 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
342
- Add JWT validation filter to embedded Jetty initial load
343

    
344
### Phase 3 — Cookie-Free Session Support
345
- 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))
346
- Add `/api/reconnect-token` endpoint to FWD server
347
- Update `p2j.screen.js` to request reconnect token via postMessage on WebSocket loss
348
- Add signed resource URL generation for top-level access (PDFs)
349
- Add `jti` replay detection (in-memory set with TTL)
350

    
351
### Phase 4 — Server Resource Access + Hardening
352
- **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.
353
- **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.
354
- Add configuration to select Option A, Option B, or both
355
- Add WebSocket `Origin` header validation
356
- Key rotation support
357
- Integration testing with genuinely cross-domain setup (different IPs, no reverse proxy)
358

    
359
---
360

    
361
## 9. What Stays Unchanged
362

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

    
371
---
372

    
373
## 10. Security Summary
374

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