Files
kme_content_adapter/specs/001-oidc-proxy-script/spec.md

111 lines
10 KiB
Markdown

# 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.