# Data Model: OIDC Proxy Script Authentication **Feature**: 001-oidc-proxy-script **Phase**: 1 — Design **Date**: 2025-07-17 --- ## Entities ### 1. AdapterSettings Persisted as `src/globalVariables/adapter_settings.json`. Loaded at server startup by `loadGlobalVariables()` and injected into every VM context as the `adapter_settings` variable. | Field | Type | Required | Description | |-------|------|----------|-------------| | `tokenUrl` | `string` | ✅ | Full HTTPS URL of the OIDC token endpoint | | `username` | `string` | ✅ | Resource owner username (ROPC grant) | | `password` | `string` | ✅ | Resource owner password | | `clientId` | `string` | ✅ | OAuth 2.0 client identifier | | `scope` | `string` | ✅ | Space-separated scopes, e.g. `"openid tags content_entitlements"` | **JSON Schema**: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["tokenUrl", "username", "password", "clientId", "scope"], "additionalProperties": false, "properties": { "tokenUrl": { "type": "string", "format": "uri" }, "username": { "type": "string", "minLength": 1 }, "password": { "type": "string", "minLength": 1 }, "clientId": { "type": "string", "minLength": 1 }, "scope": { "type": "string", "minLength": 1 } } } ``` **Example** (`src/globalVariables/adapter_settings.json`): ```json { "tokenUrl": "https://auth.kme.example.com/protocol/openid-connect/token", "username": "service-account@example.com", "password": "s3cr3t", "clientId": "kme-content-adapter", "scope": "openid tags content_entitlements" } ``` > **Security**: This file MUST be excluded from version control (`.gitignore`). > Provide `src/globalVariables/adapter_settings.json.example` with placeholder values. --- ### 2. TokenCache (Redis-backed) Persisted in Redis as a hash under the key `authorization`. Written by `proxy.js` after a successful token fetch; read on every request to check validity. | Redis field | Type (stored) | Description | |-------------|--------------|-------------| | `token` | `string` | The cached `id_token` value | | `expiry` | `string` (numeric) | Absolute Unix epoch timestamp (seconds) from `expires_in`; `"0"` or absent means expired | **Initialisation**: No pre-seeding required. `redis.hGet` returns `null` for absent fields, which proxy.js treats as expired. **In-process stampede guard** (not in Redis): ```javascript // adapter_settings._pendingFetch — set by proxy.js at runtime adapter_settings._pendingFetch = fetchPromise; // set before fetch adapter_settings._pendingFetch = null; // cleared in finally block ``` **Read pattern** (proxy.js): ```javascript const token = await redis.hGet('authorization', 'token'); const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0'); const isValid = token !== null && Date.now() / 1000 < expiry; ``` **Write pattern** (proxy.js, after successful fetch): ```javascript await redis.hSet('authorization', 'token', idToken); await redis.hSet('authorization', 'expiry', String(expiresIn)); ``` **State transitions**: ``` ┌──────────────────────────────────────────────────────────┐ │ │ ┌────────────▼──────────────┐ fetch starts ┌──────────────────┐│ │ EMPTY / EXPIRED │─────────────────────▶│ FETCHING ││ │ token: null / expiry ≤ now│ │ pendingFetch: P ││ └───────────────────────────┘ └──────┬───────────┘│ ▲ │ │ │ token expires fetch settles│ │ │ (Date.now()/1000 ≥ expiry) ┌───────────┴──────────┐ │ │ │ │ │ │ success │ failure │ │ │ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────┐│ │ │ VALID │ │ ERROR ││ └──────────────────────│ token: "abc…" │ │ token: null ││ │ expiry: 99999… │ │ expiry: 0 ││ └──────────────────┘ └──────────────┘│ │ │ └─────────────────────────────┘ (back to EMPTY / EXPIRED) ``` **Validity rule** (FR-006): ```javascript const token = await redis.hGet('authorization', 'token'); const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0'); const isValid = token !== null && Date.now() / 1000 < expiry; ``` --- ### 3. OidcTokenResponse (external — token service) Returned by the OIDC token service in response to a successful POST. Only the fields used by `proxy.js` are listed; additional fields may be present and are ignored. | Field | Type | Description | |-------|------|-------------| | `id_token` | `string` | Bearer token to cache and use for authentication (FR-004) | | `expires_in` | `number` | Absolute Unix epoch timestamp (seconds) when token expires (FR-006) | **Error response** (HTTP 4xx from token service): Not parsed; the HTTP status code alone is sufficient to trigger a 401 response to the caller. --- ### 4. ProxyResponse (outbound HTTP) The HTTP response sent by `proxy.js` back to the caller via the injected `res` object. | Scenario | Status | Content-Type | Body | |----------|--------|-------------|------| | Authentication success | `200 OK` | `text/plain` | `Authorized` | | Auth failure (any cause) | `401 Unauthorized` | `text/plain` | `Unauthorized: ` | --- ## Relationships ``` server.js │ ├── loadGlobalVariables() │ └── reads adapter_settings.json ──────────► AdapterSettings │ ├── redis (injected into VM context) ───────────► Redis Store │ └── hash: authorization │ ├── token │ └── expiry │ └── vm.createContext({ ...globalVariableContext, redis, req, res }) │ └── script.runInContext(context) [proxy.js] │ ├── reads adapter_settings ─────────► AdapterSettings ├── reads/writes redis ──────────────► Redis Store (token + expiry) ├── sets adapter_settings._pendingFetch (stampede guard, in-process) ├── POST tokenUrl ────────────────────► OIDC Token Service │ └── receives ─────────────────► OidcTokenResponse └── writes res ──────────────────────► ProxyResponse ``` --- ## Validation Rules | Rule | Location | Behaviour on violation | |------|----------|----------------------| | `tokenUrl` present | proxy.js startup | 401 with `'missing required field: tokenUrl'` | | `username` present | proxy.js startup | 401 with `'missing required field: username'` | | `password` present | proxy.js startup | 401 with `'missing required field: password'` | | `clientId` present | proxy.js startup | 401 with `'missing required field: clientId'` | | `scope` present | proxy.js startup | 401 with `'missing required field: scope'` | | `id_token` present in response | proxy.js after fetch | 401 with `'id_token missing from response'` | | `expires_in` present in response | proxy.js after fetch | 401 with `'expires_in missing from response'` | | Redis `hSet` / `hGet` available | proxy.js on every request | `ReferenceError` (server.js must inject `redis`) | > Validation errors are caught by the top-level `catch` block in proxy.js and result in a > `401 Unauthorized` response, never a process crash (SC-004).