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

197 lines
6.1 KiB
Markdown

# Quickstart: OIDC Proxy Script Authentication
**Feature**: 001-oidc-proxy-script
**Date**: 2025-07-17
---
## Prerequisites
- Node.js 18.0.0 or later (`node --version`)
- Access to a KME OIDC token service endpoint
- Project dependencies installed (`npm install`)
---
## Step 1 — Create `adapter_settings.json`
```bash
cd /path/to/kme-content-adapter
cp src/globalVariables/adapter_settings.json.example \
src/globalVariables/adapter_settings.json
```
Edit `src/globalVariables/adapter_settings.json` with your real credentials:
```json
{
"tokenUrl": "https://auth.kme.example.com/protocol/openid-connect/token",
"username": "your-service-account@example.com",
"password": "your-password",
"clientId": "your-client-id",
"scope": "openid tags content_entitlements"
}
```
> **Security**: `adapter_settings.json` is in `.gitignore`. Never commit credentials.
> The `.example` file (with placeholder values) IS committed and serves as the template.
---
## Step 2 — Start the Adapter
```bash
npm start
# or for development with auto-reload:
npm run dev
```
Expected startup log (structured JSON):
```json
{"message": "Loaded global data: adapter_settings", "keys": ["tokenUrl","username","password","clientId","scope"]}
{"message": "Loaded 1 global variables", "json": 1, "js": 0}
{"message": "Configuration loaded", "port": 3000, "host": "0.0.0.0", ...}
{"message": "Configuration validated successfully"}
{"message": "Server listening", "port": 3000, "host": "0.0.0.0"}
```
---
## Step 3 — Send a Test Request
```bash
curl -v http://localhost:3000/ProxyScript/run/67bca862210071627d32ef12/current/kmeAdapter
```
**Expected response on first request** (token fetched from OIDC service):
```
HTTP/1.1 200 OK
Content-Type: text/plain
Authorized
```
**Expected response on subsequent requests** (token served from cache):
```
HTTP/1.1 200 OK
Content-Type: text/plain
Authorized
```
No visible difference from the caller's perspective; the adapter log will show no new
axios call for the second request (cache hit).
**Expected response with invalid credentials**:
```
HTTP/1.1 401 Unauthorized
Content-Type: text/plain
Unauthorized: HTTP 401
```
---
## Step 4 — Run Tests
### Unit tests (fast, no network, no server required)
```bash
npm run test:unit
# runs: node --test tests/unit/proxy.test.js
```
Exercises: cache hit, cache miss, token expiry, stampede prevention, timeout handling,
missing `id_token`, missing required fields, HTTP error from token service.
### Contract tests (starts real HTTP server with mock token endpoint)
```bash
npm run test:contract
# runs: node --test tests/contract/proxy-http.test.js
```
Exercises: end-to-end `200 OK / Authorized`, end-to-end `401 Unauthorized`, verifies
response headers and exact body strings.
### All tests
```bash
npm test
# runs: node --test tests/**/*.test.js
```
---
## Architecture Summary
```
Inbound HTTP request
server.js (http.createServer)
│ creates fresh vm.createContext({
│ ...globalVMContext, ← axios, URLSearchParams, console, ...
│ ...globalVariableContext, ← adapter_settings (from JSON)
│ req, res ← fresh per request
│ })
proxy.js (vm.Script, compiled once)
├─ reads adapter_settings._cache
│ ├─ CACHE HIT: token valid → 200 OK / Authorized
│ └─ CACHE MISS / EXPIRED:
│ ├─ FETCHING (stampede guard): queue on _cache.pendingFetch → 200/401
│ └─ NEW FETCH:
│ POST tokenUrl (timeout: 5s)
│ ├─ SUCCESS: cache token + expiry → 200 OK / Authorized
│ └─ FAILURE: → 401 Unauthorized: <message>
HTTP response to caller
```
---
## Key Files
| File | Status | Purpose |
|------|--------|---------|
| `src/globalVariables/adapter_settings.json` | **Create** | OIDC credentials (gitignored) |
| `src/globalVariables/adapter_settings.json.example` | **Create** | Template with placeholder values |
| `src/proxyScripts/proxy.js` | **Create** | OIDC authentication proxy (VM sandbox) |
| `tests/unit/proxy.test.js` | **Create** | Unit tests (no network, no server) |
| `tests/contract/proxy-http.test.js` | **Create** | HTTP response contract tests |
| `src/server.js` | **No change** | Existing infrastructure; auto-loads adapter_settings.json |
| `config/default.json` | **No change** | Infrastructure settings only (port, host, log level) |
---
## Token Lifecycle
| Phase | What happens |
|-------|-------------|
| **First request** | `_cache.token` is null → fresh token fetch → cache `id_token` + `expires_in` |
| **Subsequent requests (valid token)** | `Date.now()/1000 < _cache.expiry` → return `Authorized` immediately, no network call |
| **After token expiry** | `Date.now()/1000 ≥ _cache.expiry` → fresh token fetch (transparent to caller) |
| **Concurrent requests during fetch** | All requests `await` the shared `_cache.pendingFetch` promise; only ONE HTTP call made |
| **Auth failure** | Clear `_cache.token` and `_cache.expiry` → respond `401 Unauthorized: <reason>` |
| **Timeout (5 s)** | axios `ECONNABORTED` error → treated as auth failure → `401 Unauthorized` |
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `401 Unauthorized: HTTP 401` | Wrong credentials | Check `username`, `password`, `clientId` in `adapter_settings.json` |
| `401 Unauthorized: connect ECONNREFUSED` | Token service unreachable | Check `tokenUrl` is correct and reachable |
| `401 Unauthorized: token service timeout` | Network slow or token service down | Verify connectivity; check token service health |
| `401 Unauthorized: id_token missing from response` | Token service returns `access_token` only | Ensure `openid` is in `scope` and the service issues `id_token` |
| Server fails to start with `adapter_settings` not found | JSON file missing | Run Step 1 above |
| `SyntaxError` in proxy.js at startup | `import` or `export` statement in proxy.js | Remove all `import`/`export` — proxy.js must be pure VM sandbox code |