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

11 KiB

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).

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.

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.

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:

{
  "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.

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.

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