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>
367 lines
11 KiB
Markdown
367 lines
11 KiB
Markdown
# 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
|