[Spec Kit] Implementation progress
This commit is contained in:
197
specs/001-oidc-proxy-script/data-model.md
Normal file
197
specs/001-oidc-proxy-script/data-model.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Data Model: OIDC Proxy Script Authentication
|
||||
|
||||
**Feature**: 001-oidc-proxy-script
|
||||
**Phase**: 1 — Design
|
||||
**Date**: 2025-07-17
|
||||
|
||||
---
|
||||
|
||||
## Entities
|
||||
|
||||
### 1. AdapterSettings
|
||||
|
||||
Persisted as `src/globalVariables/adapter_settings.json`. Loaded at server startup by
|
||||
`loadGlobalVariables()` and injected into every VM context as the `adapter_settings` variable.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `tokenUrl` | `string` | ✅ | Full HTTPS URL of the OIDC token endpoint |
|
||||
| `username` | `string` | ✅ | Resource owner username (ROPC grant) |
|
||||
| `password` | `string` | ✅ | Resource owner password |
|
||||
| `clientId` | `string` | ✅ | OAuth 2.0 client identifier |
|
||||
| `scope` | `string` | ✅ | Space-separated scopes, e.g. `"openid tags content_entitlements"` |
|
||||
|
||||
**JSON Schema**:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["tokenUrl", "username", "password", "clientId", "scope"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tokenUrl": { "type": "string", "format": "uri" },
|
||||
"username": { "type": "string", "minLength": 1 },
|
||||
"password": { "type": "string", "minLength": 1 },
|
||||
"clientId": { "type": "string", "minLength": 1 },
|
||||
"scope": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example** (`src/globalVariables/adapter_settings.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"tokenUrl": "https://auth.kme.example.com/protocol/openid-connect/token",
|
||||
"username": "service-account@example.com",
|
||||
"password": "s3cr3t",
|
||||
"clientId": "kme-content-adapter",
|
||||
"scope": "openid tags content_entitlements"
|
||||
}
|
||||
```
|
||||
|
||||
> **Security**: This file MUST be excluded from version control (`.gitignore`).
|
||||
> Provide `src/globalVariables/adapter_settings.json.example` with placeholder values.
|
||||
|
||||
---
|
||||
|
||||
### 2. TokenCache (Redis-backed)
|
||||
|
||||
Persisted in Redis as a hash under the key `authorization`. Written by `proxy.js` after
|
||||
a successful token fetch; read on every request to check validity.
|
||||
|
||||
| Redis field | Type (stored) | Description |
|
||||
|-------------|--------------|-------------|
|
||||
| `token` | `string` | The cached `id_token` value |
|
||||
| `expiry` | `string` (numeric) | Absolute Unix epoch timestamp (seconds) from `expires_in`; `"0"` or absent means expired |
|
||||
|
||||
**Initialisation**: No pre-seeding required. `redis.hGet` returns `null` for absent fields,
|
||||
which proxy.js treats as expired.
|
||||
|
||||
**In-process stampede guard** (not in Redis):
|
||||
|
||||
```javascript
|
||||
// adapter_settings._pendingFetch — set by proxy.js at runtime
|
||||
adapter_settings._pendingFetch = fetchPromise; // set before fetch
|
||||
adapter_settings._pendingFetch = null; // cleared in finally block
|
||||
```
|
||||
|
||||
**Read pattern** (proxy.js):
|
||||
|
||||
```javascript
|
||||
const token = await redis.hGet('authorization', 'token');
|
||||
const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0');
|
||||
const isValid = token !== null && Date.now() / 1000 < expiry;
|
||||
```
|
||||
|
||||
**Write pattern** (proxy.js, after successful fetch):
|
||||
|
||||
```javascript
|
||||
await redis.hSet('authorization', 'token', idToken);
|
||||
await redis.hSet('authorization', 'expiry', String(expiresIn));
|
||||
```
|
||||
|
||||
**State transitions**:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
┌────────────▼──────────────┐ fetch starts ┌──────────────────┐│
|
||||
│ EMPTY / EXPIRED │─────────────────────▶│ FETCHING ││
|
||||
│ token: null / expiry ≤ now│ │ pendingFetch: P ││
|
||||
└───────────────────────────┘ └──────┬───────────┘│
|
||||
▲ │ │
|
||||
│ token expires fetch settles│ │
|
||||
│ (Date.now()/1000 ≥ expiry) ┌───────────┴──────────┐ │
|
||||
│ │ │ │
|
||||
│ success │ failure │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────────┐ ┌──────────────┐│
|
||||
│ │ VALID │ │ ERROR ││
|
||||
└──────────────────────│ token: "abc…" │ │ token: null ││
|
||||
│ expiry: 99999… │ │ expiry: 0 ││
|
||||
└──────────────────┘ └──────────────┘│
|
||||
│ │
|
||||
└─────────────────────────────┘
|
||||
(back to EMPTY / EXPIRED)
|
||||
```
|
||||
|
||||
**Validity rule** (FR-006):
|
||||
|
||||
```javascript
|
||||
const token = await redis.hGet('authorization', 'token');
|
||||
const expiry = parseFloat(await redis.hGet('authorization', 'expiry') ?? '0');
|
||||
const isValid = token !== null && Date.now() / 1000 < expiry;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. OidcTokenResponse (external — token service)
|
||||
|
||||
Returned by the OIDC token service in response to a successful POST. Only the fields used by
|
||||
`proxy.js` are listed; additional fields may be present and are ignored.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id_token` | `string` | Bearer token to cache and use for authentication (FR-004) |
|
||||
| `expires_in` | `number` | Absolute Unix epoch timestamp (seconds) when token expires (FR-006) |
|
||||
|
||||
**Error response** (HTTP 4xx from token service): Not parsed; the HTTP status code alone is
|
||||
sufficient to trigger a 401 response to the caller.
|
||||
|
||||
---
|
||||
|
||||
### 4. ProxyResponse (outbound HTTP)
|
||||
|
||||
The HTTP response sent by `proxy.js` back to the caller via the injected `res` object.
|
||||
|
||||
| Scenario | Status | Content-Type | Body |
|
||||
|----------|--------|-------------|------|
|
||||
| Authentication success | `200 OK` | `text/plain` | `Authorized` |
|
||||
| Auth failure (any cause) | `401 Unauthorized` | `text/plain` | `Unauthorized: <message>` |
|
||||
|
||||
---
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
server.js
|
||||
│
|
||||
├── loadGlobalVariables()
|
||||
│ └── reads adapter_settings.json ──────────► AdapterSettings
|
||||
│
|
||||
├── redis (injected into VM context) ───────────► Redis Store
|
||||
│ └── hash: authorization
|
||||
│ ├── token
|
||||
│ └── expiry
|
||||
│
|
||||
└── vm.createContext({ ...globalVariableContext, redis, req, res })
|
||||
│
|
||||
└── script.runInContext(context) [proxy.js]
|
||||
│
|
||||
├── reads adapter_settings ─────────► AdapterSettings
|
||||
├── reads/writes redis ──────────────► Redis Store (token + expiry)
|
||||
├── sets adapter_settings._pendingFetch (stampede guard, in-process)
|
||||
├── POST tokenUrl ────────────────────► OIDC Token Service
|
||||
│ └── receives ─────────────────► OidcTokenResponse
|
||||
└── writes res ──────────────────────► ProxyResponse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
| Rule | Location | Behaviour on violation |
|
||||
|------|----------|----------------------|
|
||||
| `tokenUrl` present | proxy.js startup | 401 with `'missing required field: tokenUrl'` |
|
||||
| `username` present | proxy.js startup | 401 with `'missing required field: username'` |
|
||||
| `password` present | proxy.js startup | 401 with `'missing required field: password'` |
|
||||
| `clientId` present | proxy.js startup | 401 with `'missing required field: clientId'` |
|
||||
| `scope` present | proxy.js startup | 401 with `'missing required field: scope'` |
|
||||
| `id_token` present in response | proxy.js after fetch | 401 with `'id_token missing from response'` |
|
||||
| `expires_in` present in response | proxy.js after fetch | 401 with `'expires_in missing from response'` |
|
||||
| Redis `hSet` / `hGet` available | proxy.js on every request | `ReferenceError` (server.js must inject `redis`) |
|
||||
|
||||
> Validation errors are caught by the top-level `catch` block in proxy.js and result in a
|
||||
> `401 Unauthorized` response, never a process crash (SC-004).
|
||||
Reference in New Issue
Block a user