8.9 KiB
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:
{
"$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):
{
"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). Providesrc/globalVariables/adapter_settings.json.examplewith 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):
// 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):
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):
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):
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: <message> |
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
catchblock in proxy.js and result in a401 Unauthorizedresponse, never a process crash (SC-004).