Files
SentryAgent.ai Developer f1fbe0e29a 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>
2026-04-02 03:50:47 +00:00

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