# Feature Specification: OIDC Proxy Script Authentication **Feature Branch**: `001-oidc-proxy-script` **Created**: 2025-07-16 **Status**: Clarified ## User Scenarios & Testing *(mandatory)* ### User Story 1 - Successful Authenticated Request (Priority: P1) A system operator or integration consumer sends an HTTP request through the kme-content-adapter. The proxy script transparently authenticates against the KME OIDC token service using stored credentials and, upon success, returns a confirmation response to the caller. **Why this priority**: This is the core behaviour of the feature — without successful authentication, the proxy script provides no value. All other stories depend on this flow working correctly. **Independent Test**: Can be tested in isolation by sending any HTTP request to the adapter's proxy endpoint and confirming a `200 OK` response with an "Authorized" body is returned, verifiable without any downstream system interaction. **Acceptance Scenarios**: 1. **Given** the adapter is running and `adapter_settings.json` contains valid credentials and a reachable token URL, **When** an HTTP request arrives at the proxy endpoint, **Then** the script obtains a valid OIDC token and responds with HTTP `200 OK` and the body `Authorized`. 2. **Given** a valid OIDC token has already been cached and has not expired, **When** a subsequent HTTP request arrives, **Then** the script reuses the cached token without making a new authentication request, and responds with HTTP `200 OK` and the body `Authorized`. --- ### User Story 2 - Token Expiry and Refresh (Priority: P2) The system manages token lifetime transparently. When a previously cached token has expired, the proxy script automatically obtains a fresh token before responding. **Why this priority**: Without expiry handling, the proxy fails silently after the token lifetime ends, causing all requests to break until the adapter is restarted. **Independent Test**: Can be tested by simulating a cached token with a past expiry time and confirming the script fetches a new token and still returns `200 OK`. **Acceptance Scenarios**: 1. **Given** a cached token whose expiry time has passed, **When** a new HTTP request arrives, **Then** the script discards the expired token, fetches a fresh one from the token service, and responds with HTTP `200 OK` and the body `Authorized`. 2. **Given** a cached token that is still valid, **When** checking expiry, **Then** no new token request is made to the token service. --- ### User Story 3 - Authentication Failure Handling (Priority: P3) If the token service rejects the credentials or is unreachable, the proxy script communicates the failure clearly to the caller rather than hanging or returning a misleading success. **Why this priority**: Proper error surfacing prevents silent failures that are difficult to diagnose in production. **Independent Test**: Can be tested by providing invalid credentials in `adapter_settings.json` and confirming the proxy returns an appropriate HTTP error response. **Acceptance Scenarios**: 1. **Given** the credentials in `adapter_settings.json` are invalid, **When** an HTTP request arrives, **Then** the proxy script responds with HTTP `401 Unauthorized` and an error message without crashing the adapter process. 2. **Given** the token service URL is unreachable, **When** an HTTP request arrives, **Then** the proxy script responds with HTTP `401 Unauthorized` and a descriptive error message. --- ### Edge Cases - What happens when `adapter_settings.json` is missing the `tokenUrl`, `username`, or `password` fields? - How does the system handle a token service response that omits the `id_token` field? - **[RESOLVED]** If `expires_in` is already in the past on arrival, treat as expired and fetch a fresh token immediately. - **[RESOLVED]** When two concurrent requests arrive while no valid token is cached (token stampede), only one token fetch is made; all others queue and share the result. ## Requirements *(mandatory)* ### Functional Requirements - **FR-001**: The proxy script MUST read `tokenUrl`, `username`, `password`, `clientId`, and `scope` exclusively from the `adapter_settings` injected variable (sourced from `src/globalVariables/adapter_settings.json`). - **FR-002**: `src/globalVariables/adapter_settings.json` MUST contain the fields `tokenUrl`, `username`, `password`, `clientId`, and `scope`. - **FR-003**: The proxy script MUST authenticate by sending a `POST` request to the configured `tokenUrl` with a `Content-Type: application/x-www-form-urlencoded` body containing `grant_type=password`, `username`, `password`, `client_id`, and `scope`. - **FR-004**: The proxy script MUST extract the bearer token from the `id_token` field of the token service's JSON response. - **FR-005**: The proxy script MUST cache the obtained token in Redis using `redis.hSet('authorization', 'token', token)` and `redis.hSet('authorization', 'expiry', String(expires_in))`, and reuse it for subsequent requests until it expires. - **FR-006**: The proxy script MUST determine token expiry by reading `redis.hGet('authorization', 'expiry')` and comparing to `Date.now() / 1000`. If the stored expiry is already in the past on read, the token MUST be treated as expired and a fresh token fetched immediately. - **FR-007**: The proxy script MUST respond with HTTP `200 OK` and the plain-text body `Authorized` when authentication succeeds. - **FR-008**: The proxy script MUST respond with HTTP `401 Unauthorized` and a descriptive plain-text message when authentication fails (invalid credentials, unreachable token service, or malformed response). - **FR-009**: The proxy script file (`src/proxyScripts/proxy.js`) MUST contain zero `import` or `export` statements, as it executes inside a Node.js VM sandbox. - **FR-010**: The proxy script MUST NOT reference `config`, `global.config`, or `process.env` for any configuration or credential values. - **FR-011**: The proxy script MUST use only dependencies injected via the VM context: `axios`, `console`, `crypto`, `jwt`, `uuidv4`, `xmlBuilder`, `URLSearchParams`, `URL`, and `redis`. - **FR-012**: `req` and `res` must be treated as the injected Node.js HTTP request and response objects; no other I/O mechanism may be used. - **FR-013**: When two or more concurrent requests arrive while no valid token is cached, only one token fetch request MUST be made to the token service; all other requests MUST queue and share the result of that single fetch. - **FR-014**: The token POST request to the OIDC service MUST apply a 5-second HTTP timeout; a timeout error MUST be treated as an authentication failure (FR-008). ### Key Entities - **adapter_settings**: Configuration object loaded from `src/globalVariables/adapter_settings.json` and injected into the VM context. Contains `tokenUrl`, `username`, `password`, `clientId`, and `scope`. - **OIDC Token**: A short-lived bearer token issued by the KME OIDC token service. Identified by its `id_token` field; lifetime communicated via `expires_in` (Unix epoch seconds). - **Token Cache**: Token and expiry stored in Redis under the hash key `authorization` (fields `token` and `expiry`). An in-process `pendingFetch` property on `adapter_settings` guards against stampede within the single adapter process (Promises cannot be serialised to Redis). - **Proxy Request/Response**: The Node.js `req` and `res` HTTP objects injected into the VM, representing the inbound caller and the outbound reply. ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001**: Every inbound request to the proxy endpoint receives a response (success or error) within 5 seconds under normal network conditions. - **SC-002**: After the first successful authentication, all subsequent requests with a valid cached token complete without contacting the token service, reducing per-request authentication overhead to zero network round-trips. - **SC-003**: Token refresh occurs automatically with no manual intervention required when a cached token expires; the caller receives `200 OK` transparently. - **SC-004**: 100% of authentication failures (bad credentials, network errors, malformed responses) result in HTTP `401 Unauthorized` rather than an unhandled exception or process crash. - **SC-005**: The proxy script introduces no new global state, file system access, or environment variable reads — verifiable by static inspection of the file (zero import/export/process.env/config references). ## Assumptions - The KME OIDC token service's `expires_in` field represents an **absolute Unix epoch timestamp in seconds** (not a relative duration in seconds), as implied by the example value `1532618185`. Expiry check: `Date.now() / 1000 < expires_in`. If the value is already past on receipt, the token is immediately considered expired. - Token and expiry are persisted in Redis (`redis.hSet / hGet` on hash key `authorization`), surviving adapter restarts and shared across any future processes. - The in-process `pendingFetch` stampede guard is retained on `adapter_settings` because Promises cannot be serialised to Redis; this is appropriate for the current single-process deployment. - Concurrent requests during a token fetch are queued; only one fetch is in-flight at any time (no stampede). - Authentication failures return HTTP `401 Unauthorized` with a plain-text error body. - The token POST request carries a 5-second timeout. - The adapter process remains long-running; module-level variable caching is therefore effective across multiple requests without requiring an external cache. - Only one token is needed at a time; there is no multi-tenant or per-user token requirement for this proxy script. - The `scope` value `openid tags content_entitlements` is fixed and not expected to vary per request. - The caller of the proxy endpoint does not require the actual OIDC token in the response body; the `200 OK / Authorized` reply is sufficient to confirm authentication succeeded. - Error responses should be plain text to keep the script simple; no structured error body format is required. - The VM context is always initialised with all listed dependencies (`axios`, `console`, `crypto`, `jwt`, `uuidv4`, `xmlBuilder`, `URLSearchParams`, `URL`) before the script executes.