Files

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). 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):

// 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 catch block in proxy.js and result in a 401 Unauthorized response, never a process crash (SC-004).