- 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>
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:
- Given the adapter is running and
adapter_settings.jsoncontains 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 HTTP200 OKand the bodyAuthorized. - 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 OKand the bodyAuthorized.
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:
- 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 OKand the bodyAuthorized. - 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:
- Given the credentials in
adapter_settings.jsonare invalid, When an HTTP request arrives, Then the proxy script responds with HTTP401 Unauthorizedand an error message without crashing the adapter process. - Given the token service URL is unreachable, When an HTTP request arrives, Then the proxy script responds with HTTP
401 Unauthorizedand a descriptive error message.
Edge Cases
- What happens when
adapter_settings.jsonis missing thetokenUrl,username, orpasswordfields? - How does the system handle a token service response that omits the
id_tokenfield? - [RESOLVED] If
expires_inis 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, andscopeexclusively from theadapter_settingsinjected variable (sourced fromsrc/globalVariables/adapter_settings.json). - FR-002:
src/globalVariables/adapter_settings.jsonMUST contain the fieldstokenUrl,username,password,clientId, andscope. - FR-003: The proxy script MUST authenticate by sending a
POSTrequest to the configuredtokenUrlwith aContent-Type: application/x-www-form-urlencodedbody containinggrant_type=password,username,password,client_id, andscope. - FR-004: The proxy script MUST extract the bearer token from the
id_tokenfield 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)andredis.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 toDate.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 OKand the plain-text bodyAuthorizedwhen authentication succeeds. - FR-008: The proxy script MUST respond with HTTP
401 Unauthorizedand 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 zeroimportorexportstatements, as it executes inside a Node.js VM sandbox. - FR-010: The proxy script MUST NOT reference
config,global.config, orprocess.envfor 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, andredis. - FR-012:
reqandresmust 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.jsonand injected into the VM context. ContainstokenUrl,username,password,clientId, andscope. - OIDC Token: A short-lived bearer token issued by the KME OIDC token service. Identified by its
id_tokenfield; lifetime communicated viaexpires_in(Unix epoch seconds). - Token Cache: Token and expiry stored in Redis under the hash key
authorization(fieldstokenandexpiry). An in-processpendingFetchproperty onadapter_settingsguards against stampede within the single adapter process (Promises cannot be serialised to Redis). - Proxy Request/Response: The Node.js
reqandresHTTP 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 OKtransparently. - SC-004: 100% of authentication failures (bad credentials, network errors, malformed responses) result in HTTP
401 Unauthorizedrather 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_infield represents an absolute Unix epoch timestamp in seconds (not a relative duration in seconds), as implied by the example value1532618185. 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 / hGeton hash keyauthorization), surviving adapter restarts and shared across any future processes. - The in-process
pendingFetchstampede guard is retained onadapter_settingsbecause 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 Unauthorizedwith 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
scopevalueopenid tags content_entitlementsis 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 / Authorizedreply 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.