6.1 KiB
Implementation Plan: OIDC Proxy Script Authentication
Branch: 001-oidc-proxy-script | Date: 2025-07-17 | Spec: spec.md
Input: Feature specification from specs/001-oidc-proxy-script/spec.md
Summary
Create src/globalVariables/adapter_settings.json with OIDC credentials and implement
src/proxyScripts/proxy.js — a zero-import/export Node.js VM-sandbox script that:
- Reads OIDC credentials exclusively from the injected
adapter_settingscontext variable (FR-001) - POSTs to the configured
tokenUrlwithapplication/x-www-form-urlencodedbody and a 5-second timeout (FR-003, FR-014) - Extracts the bearer token from the
id_tokenfield of the JSON response (FR-004) - Caches the token in Redis (
redis.hSet('authorization', 'token', ...)/redis.hSet('authorization', 'expiry', ...)) usingexpires_inas an absolute Unix epoch timestamp (FR-005, FR-006) - Queues concurrent callers on a shared promise to prevent token-fetch stampedes (FR-013)
- Returns
200 OK / Authorizedon success and401 Unauthorizedwith a descriptive message on any failure (FR-007, FR-008)
No modifications to server.js are required. The existing loadGlobalVariables() pattern
automatically picks up adapter_settings.json and injects it as adapter_settings into every VM
context.
Technical Context
Language/Version: Node.js 18+ (ES Modules; "type": "module" in package.json)
Primary Dependencies: axios ^1.13.6, uuid ^13.0.0, jsonwebtoken ^9.0.3,
xmlbuilder2 ^4.0.3 — all already present in package.json; no new packages required
Storage: Redis — token and expiry stored in hash key authorization via redis.hSet/hGet; redis is injected into the VM context as a global. An in-process adapter_settings._pendingFetch guards against stampede (Promises cannot be serialised to Redis).
Testing: Node.js built-in node:test runner (node --test tests/**/*.test.js);
t.mock.fn(), t.mock.timers for fakes; no external test framework needed
Target Platform: Linux/macOS, long-running Node.js 18+ server process
Project Type: HTTP proxy adapter — VM-sandboxed script (IVA Studio proxy script pattern)
Performance Goals: Every request responds within 5 s (SC-001); zero token-service round-trips
when a valid cached token exists (SC-002)
Constraints: proxy.js MUST have zero import/export statements; MUST NOT reference
config, global.config, or process.env; all dependencies injected via vm.createContext()
Scale/Scope: Single-process, single-tenant; one OIDC token shared across all concurrent
requests
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle | Status | Notes |
|---|---|---|
| I. Monolithic Architecture | ✅ PASS | All auth + cache logic in proxy.js; no helper extraction |
| I. Zero Imports/Exports | ✅ PASS | proxy.js uses only VM-injected globals; zero import/export |
| I.0 Forbidden Globals | ✅ PASS | No config, global.config, or process.env in proxy.js |
| I.I What MUST be in proxy.js | ✅ PASS | Authentication, token cache, stampede queue all in proxy.js |
| I.II Allowed Separate Files | ✅ PASS | Only adding adapter_settings.json to src/globalVariables/ |
| I.IV Configuration | ✅ PASS | Credentials in src/globalVariables/adapter_settings.json, not config/default.json |
| I.V VM Context Injection | ✅ PASS | adapter_settings auto-loaded by existing loadGlobalVariables() — no server.js changes |
| II. API-First Design | ✅ PASS | HTTP response contract and VM context contract documented before implementation |
| III. Test-First Development | ✅ PASS | Test scenarios defined; tests written before implementation code |
No violations. Complexity Tracking not required.
Post-Design Re-check
| Principle | Status | Notes |
|---|---|---|
| Cache state isolation | ✅ PASS | Token/expiry stored in Redis hash authorization; in-process adapter_settings._pendingFetch holds stampede guard (Promise not serialisable to Redis) |
| Cross-realm Promise | ✅ PASS | Stampede guard uses duck-type check (typeof .then === 'function') rather than instanceof Promise; await uses the Promises/A+ thenable protocol which is cross-realm safe |
| Async error containment | ✅ PASS | script.runInContext() is not awaited by server.js; proxy.js top-level async IIFE catches all errors internally and always sends a response |
Project Structure
Documentation (this feature)
specs/001-oidc-proxy-script/
├── plan.md # This file
├── research.md # Phase 0 — resolved unknowns
├── data-model.md # Phase 1 — entities and state transitions
├── quickstart.md # Phase 1 — setup and testing guide
├── contracts/
│ ├── proxy-http.md # HTTP response contract (success + error)
│ └── vm-context.md # VM dependency injection contract
└── tasks.md # Phase 2 — task list (/speckit.tasks, not created here)
Source Code (repository root)
src/
├── proxyScripts/
│ └── proxy.js # NEW — OIDC authentication proxy (VM sandbox)
├── globalVariables/
│ └── adapter_settings.json # NEW — OIDC credentials and token endpoint
├── logger.js # EXISTING — no changes
└── server.js # EXISTING — no changes required
tests/
├── unit/
│ └── proxy.test.js # NEW — unit tests: cache, expiry, stampede, error paths
└── contract/
└── proxy-http.test.js # NEW — contract tests: HTTP 200/401 response shape
Structure Decision: Single-project layout (Option 1). Two new source files, two new test
files. No new directories (both src/proxyScripts/ and src/globalVariables/ already exist as
defined directories in the constitution; tests/unit/ and tests/contract/ match the existing
package.json test scripts).
Complexity Tracking
No constitution violations — this section is intentionally empty.