[Spec Kit] Implementation progress
This commit is contained in:
196
specs/001-oidc-proxy-script/quickstart.md
Normal file
196
specs/001-oidc-proxy-script/quickstart.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user