Files
kme_content_adapter/specs/001-oidc-proxy-script/spec.md
Peter.Morton f840587e5e feat: content fetch, sitemap fixes, remove oidcAuthFlow
- Add contentFetchFlow() to proxy (FR-001 through FR-012)
- Add extractArticleBody() helper with vkm:articleBody / articleBody fallback
- Dynamic proxyBaseUrl derivation from x-forwarded-proto/host headers
- Forward query/size/category params on /sitemap.xml requests
- Add Accept: application/ld+json header to content API calls
- Remove oidcAuthFlow() - unmatched requests now return 404 Not Found
- Fix xmlbuilder2 import: default import, call as xmlbuilder2.create(...)
- Version bump 0.2.0 → 0.3.0
- 45/45 tests passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 16:40:06 -05:00

10 KiB

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, xmlbuilder2, 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, xmlbuilder2, URLSearchParams, URL) before the script executes.