chore(openspec): archive all completed changes, sync 14 new specs to library
Archived 4 completed OpenSpec changes (2026-04-02): - phase-3-enterprise (100/100 tasks) — 6 Phase 3 capabilities synced - devops-documentation (48/48 tasks) — 3 new + 1 merged capability - bedroom-developer-docs (33/33 tasks) — 4 new capabilities synced - engineering-docs (superseded by 2026-03-29 archive) — no tasks Main spec library grows from 21 → 35 capabilities (+14 new): federation, multi-tenancy, oidc, soc2, w3c-dids, webhooks, database, operations, system-overview, api-reference, core-concepts, developer-guides, quick-start + deployment (merged additive requirements) Active changes: 0 — project board is clear for Phase 4 planning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
366
openspec/specs/oidc/spec.md
Normal file
366
openspec/specs/oidc/spec.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# OpenID Connect (OIDC) — Specification
|
||||
|
||||
**Workstream**: 3 of 6
|
||||
**Phase**: 3 — Enterprise
|
||||
**Author**: Virtual Architect
|
||||
**Date**: 2026-03-29
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Add a full OIDC 1.0 layer on top of the existing OAuth 2.0 `client_credentials` implementation using the certified `oidc-provider` npm library. The OIDC layer exposes Discovery, JWKS, extends the token endpoint to return ID tokens with agent claims, and provides an `/agent-info` endpoint (the agent-identity equivalent of OIDC's `/userinfo`).
|
||||
|
||||
The existing `POST /oauth2/token` endpoint is extended, not replaced. Callers that do not request the `openid` scope continue to receive standard OAuth 2.0 responses unchanged.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /.well-known/openid-configuration
|
||||
|
||||
OIDC Discovery document. No authentication required. This is the standard OIDC Discovery endpoint (RFC 8414 / OpenID Connect Discovery 1.0).
|
||||
|
||||
```yaml
|
||||
GET /.well-known/openid-configuration
|
||||
No authentication required
|
||||
|
||||
Responses:
|
||||
200 OK:
|
||||
Content-Type: application/json
|
||||
schema:
|
||||
type: object
|
||||
description: OIDC Discovery document per OpenID Connect Discovery 1.0
|
||||
example:
|
||||
issuer: "https://idp.sentryagent.ai"
|
||||
authorization_endpoint: "https://idp.sentryagent.ai/oauth2/authorize"
|
||||
token_endpoint: "https://idp.sentryagent.ai/oauth2/token"
|
||||
jwks_uri: "https://idp.sentryagent.ai/.well-known/jwks.json"
|
||||
userinfo_endpoint: "https://idp.sentryagent.ai/agent-info"
|
||||
introspection_endpoint: "https://idp.sentryagent.ai/oauth2/introspect"
|
||||
revocation_endpoint: "https://idp.sentryagent.ai/oauth2/revoke"
|
||||
response_types_supported:
|
||||
- "token"
|
||||
grant_types_supported:
|
||||
- "client_credentials"
|
||||
subject_types_supported:
|
||||
- "public"
|
||||
id_token_signing_alg_values_supported:
|
||||
- "RS256"
|
||||
- "ES256"
|
||||
scopes_supported:
|
||||
- "openid"
|
||||
- "agents:read"
|
||||
- "agents:write"
|
||||
- "tokens:read"
|
||||
- "audit:read"
|
||||
claims_supported:
|
||||
- "sub"
|
||||
- "iss"
|
||||
- "iat"
|
||||
- "exp"
|
||||
- "agent_id"
|
||||
- "agent_type"
|
||||
- "organization_id"
|
||||
- "capabilities"
|
||||
- "deployment_env"
|
||||
- "owner"
|
||||
token_endpoint_auth_methods_supported:
|
||||
- "client_secret_post"
|
||||
- "client_secret_basic"
|
||||
500 Internal Server Error:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /.well-known/jwks.json
|
||||
|
||||
JSON Web Key Set. Contains the public keys used to sign ID tokens and access tokens. No authentication required. Clients use this endpoint to verify token signatures.
|
||||
|
||||
```yaml
|
||||
GET /.well-known/jwks.json
|
||||
No authentication required
|
||||
|
||||
Responses:
|
||||
200 OK:
|
||||
Content-Type: application/json
|
||||
Cache-Control: public, max-age=3600
|
||||
schema:
|
||||
type: object
|
||||
required: [keys]
|
||||
properties:
|
||||
keys:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
description: JSON Web Key (RFC 7517)
|
||||
properties:
|
||||
kty:
|
||||
type: string
|
||||
example: "RSA"
|
||||
use:
|
||||
type: string
|
||||
example: "sig"
|
||||
kid:
|
||||
type: string
|
||||
description: Key ID — matches `kid` header in issued JWTs
|
||||
alg:
|
||||
type: string
|
||||
example: "RS256"
|
||||
n:
|
||||
type: string
|
||||
description: RSA modulus (base64url)
|
||||
e:
|
||||
type: string
|
||||
description: RSA exponent (base64url)
|
||||
example:
|
||||
keys:
|
||||
- kty: "RSA"
|
||||
use: "sig"
|
||||
kid: "key-2026-03-29-01"
|
||||
alg: "RS256"
|
||||
n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt..."
|
||||
e: "AQAB"
|
||||
500 Internal Server Error:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /oauth2/token (extended)
|
||||
|
||||
The existing token endpoint is extended to return an `id_token` when the `openid` scope is requested. All existing behavior is preserved when `openid` is not in the scope list.
|
||||
|
||||
```yaml
|
||||
POST /oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
Request Body:
|
||||
schema:
|
||||
type: object
|
||||
required: [grant_type, client_id, client_secret]
|
||||
properties:
|
||||
grant_type:
|
||||
type: string
|
||||
enum: [client_credentials]
|
||||
client_id:
|
||||
type: string
|
||||
client_secret:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated scopes. Include "openid" to receive an id_token.
|
||||
example: "openid agents:read"
|
||||
|
||||
Responses:
|
||||
200 OK (with openid scope):
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
token_type:
|
||||
type: string
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
scope:
|
||||
type: string
|
||||
id_token:
|
||||
type: string
|
||||
description: Signed JWT ID token containing agent identity claims. Only present when openid scope was requested.
|
||||
example:
|
||||
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
token_type: "Bearer"
|
||||
expires_in: 3600
|
||||
scope: "openid agents:read"
|
||||
id_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0yMDI2LTAzLTI5LTAxIn0..."
|
||||
|
||||
200 OK (without openid scope):
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
token_type:
|
||||
type: string
|
||||
expires_in:
|
||||
type: integer
|
||||
scope:
|
||||
type: string
|
||||
example:
|
||||
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
token_type: "Bearer"
|
||||
expires_in: 3600
|
||||
scope: "agents:read"
|
||||
|
||||
400 Bad Request:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuthErrorResponse'
|
||||
example:
|
||||
error: "invalid_client"
|
||||
error_description: "Invalid client credentials"
|
||||
|
||||
401 Unauthorized:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuthErrorResponse'
|
||||
```
|
||||
|
||||
#### ID Token Claims
|
||||
|
||||
When `openid` scope is requested, the ID token (a signed JWT) contains the following claims:
|
||||
|
||||
```json
|
||||
{
|
||||
"iss": "https://idp.sentryagent.ai",
|
||||
"sub": "agt_01HXK7Z9P3FKWABCDEF67890",
|
||||
"aud": "agt_01HXK7Z9P3FKWABCDEF67890",
|
||||
"iat": 1743249600,
|
||||
"exp": 1743253200,
|
||||
"agent_id": "agt_01HXK7Z9P3FKWABCDEF67890",
|
||||
"agent_type": "orchestrator",
|
||||
"organization_id": "org_01HXK7Z9P3FKWABCDEF12345",
|
||||
"capabilities": ["task-planning", "tool-use"],
|
||||
"deployment_env": "production",
|
||||
"owner": "acme-ai",
|
||||
"did": "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /agent-info
|
||||
|
||||
Returns claims about the authenticated agent identity. This is the agent-first equivalent of the OIDC `/userinfo` endpoint. Authentication required with any valid access token.
|
||||
|
||||
```yaml
|
||||
GET /agent-info
|
||||
Authorization: Bearer <access_token>
|
||||
|
||||
Responses:
|
||||
200 OK:
|
||||
Content-Type: application/json
|
||||
schema:
|
||||
type: object
|
||||
description: Agent identity claims (subset of registered agent data)
|
||||
properties:
|
||||
sub:
|
||||
type: string
|
||||
description: Subject — agentId
|
||||
agent_id:
|
||||
type: string
|
||||
agent_type:
|
||||
type: string
|
||||
organization_id:
|
||||
type: string
|
||||
capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
deployment_env:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
did:
|
||||
type: string
|
||||
description: W3C DID for this agent (if DID workstream is active)
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example:
|
||||
sub: "agt_01HXK7Z9P3FKWABCDEF67890"
|
||||
agent_id: "agt_01HXK7Z9P3FKWABCDEF67890"
|
||||
agent_type: "orchestrator"
|
||||
organization_id: "org_01HXK7Z9P3FKWABCDEF12345"
|
||||
capabilities: ["task-planning", "tool-use"]
|
||||
deployment_env: "production"
|
||||
owner: "acme-ai"
|
||||
version: "1.2.0"
|
||||
status: "active"
|
||||
did: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
||||
created_at: "2026-03-29T12:00:00Z"
|
||||
401 Unauthorized:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "UNAUTHORIZED"
|
||||
message: "Invalid or expired access token"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Changes
|
||||
|
||||
### New Table: oidc_keys
|
||||
|
||||
Stores the RSA/EC key pairs used for ID token signing. Private keys stored in Vault; public key JWK in PostgreSQL for JWKS endpoint.
|
||||
|
||||
```sql
|
||||
CREATE TABLE oidc_keys (
|
||||
key_id VARCHAR(40) PRIMARY KEY,
|
||||
kid VARCHAR(100) NOT NULL UNIQUE, -- Key ID in JWKS
|
||||
algorithm VARCHAR(10) NOT NULL,
|
||||
use_purpose VARCHAR(10) NOT NULL DEFAULT 'sig',
|
||||
public_key_jwk JSONB NOT NULL,
|
||||
vault_key_path VARCHAR(255) NOT NULL,
|
||||
is_current BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
retired_at TIMESTAMPTZ,
|
||||
CONSTRAINT oidc_keys_alg_check CHECK (algorithm IN ('RS256', 'ES256')),
|
||||
CONSTRAINT oidc_keys_use_check CHECK (use_purpose IN ('sig', 'enc'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oidc_keys_is_current ON oidc_keys(is_current) WHERE is_current = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|---------------------|-------------|---------|
|
||||
| `OIDC_ISSUER` | OIDC issuer URL (must match token `iss` claim) | `https://${HOST}` |
|
||||
| `OIDC_ID_TOKEN_TTL_SECONDS` | ID token lifetime | `3600` |
|
||||
| `OIDC_SIGNING_ALG` | ID token signing algorithm | `RS256` |
|
||||
| `OIDC_JWKS_CACHE_TTL_SECONDS` | JWKS response cache TTL | `3600` |
|
||||
| `OIDC_KEY_ROTATION_DAYS` | Days between signing key rotations | `90` |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `oidc-provider` | `^8.4.6` | Certified OIDC server library (OpenID Foundation conformant) |
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- ID token signing keys are stored in Vault; public keys only are served via JWKS
|
||||
- JWKS endpoint is cached in Redis (`OIDC_JWKS_CACHE_TTL_SECONDS`) to prevent key-hammering
|
||||
- Key rotation: when a new signing key is created, the old key remains in JWKS until all tokens signed with it have expired
|
||||
- The `openid` scope is only issued to callers explicitly requesting it — not included by default
|
||||
- `GET /agent-info` returns the same data as the ID token — no additional sensitive data
|
||||
- ID tokens for agent credentials must not contain client secrets or internal system paths
|
||||
- `alg: none` is explicitly rejected — all ID tokens must be signed
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `/.well-known/openid-configuration` passes OIDC Discovery conformance validation
|
||||
- [ ] `/.well-known/jwks.json` returns valid JWKS with current signing public key
|
||||
- [ ] ID token returned when `openid` scope is in token request; not returned otherwise
|
||||
- [ ] ID token is verifiable against JWKS endpoint using standard JWT libraries
|
||||
- [ ] ID token claims match agent record (agent_type, capabilities, organization_id, did)
|
||||
- [ ] `/agent-info` returns correct claims for authenticated agent
|
||||
- [ ] Key rotation: old JWKS key is kept until all signed tokens expire
|
||||
- [ ] TypeScript strict, zero `any`, >80% test coverage on OIDCService
|
||||
Reference in New Issue
Block a user