Files
sentryagent-idp/openspec/changes/phase-1-mvp-implementation/specs/oauth2-token/spec.md
SentryAgent.ai Developer d3530285b9 feat: Phase 1 MVP — complete AgentIdP implementation
Implements all P0 features per OpenSpec change phase-1-mvp-implementation:
- Agent Registry Service (CRUD) — full lifecycle management
- OAuth 2.0 Token Service (Client Credentials flow)
- Credential Management (generate, rotate, revoke)
- Immutable Audit Log Service

Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+
Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types
Quality: 18 unit test suites, 244 tests passing, 97%+ coverage
OpenAPI: 4 complete specs (14 endpoints total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 09:14:41 +00:00

5.6 KiB

ADDED Requirements

Requirement: Issue access token via Client Credentials grant

The system SHALL issue a signed RS256 JWT access token when an agent authenticates with a valid client_id (agentId) and client_secret using the OAuth 2.0 Client Credentials grant (RFC 6749 §4.4). The request body SHALL use application/x-www-form-urlencoded encoding. The response SHALL include Cache-Control: no-store and Pragma: no-cache headers. The system SHALL enforce a free-tier limit of 10,000 token requests per calendar month per client.

Scenario: Successful token issuance

  • WHEN a POST request to /token is received with grant_type=client_credentials, a valid client_id, and a valid client_secret for an active agent
  • THEN the system verifies the credential, issues a signed JWT with sub = agentId, scope = requested (or default) scope, exp = now + 3600s, and returns 200 OK with TokenResponse

Scenario: Invalid client credentials rejected

  • WHEN a POST request to /token is received with a client_id that does not exist or a client_secret that does not match
  • THEN the system returns 401 Unauthorized with error: invalid_client

Scenario: Suspended agent cannot obtain tokens

  • WHEN a POST request to /token is received for an agent with status: suspended
  • THEN the system returns 403 Forbidden with error: unauthorized_client and a description indicating the agent is suspended

Scenario: Decommissioned agent cannot obtain tokens

  • WHEN a POST request to /token is received for an agent with status: decommissioned
  • THEN the system returns 403 Forbidden with error: unauthorized_client

Scenario: Unsupported grant type rejected

  • WHEN a POST request to /token is received with a grant_type other than client_credentials
  • THEN the system returns 400 Bad Request with error: unsupported_grant_type

Scenario: Invalid scope rejected

  • WHEN a POST request to /token is received with a scope value that contains an unrecognised scope identifier
  • THEN the system returns 400 Bad Request with error: invalid_scope

Scenario: Free tier monthly token limit enforced

  • WHEN a POST request to /token is received and the agent has already made 10,000 token requests in the current calendar month
  • THEN the system returns 403 Forbidden with error: unauthorized_client and a description indicating the monthly free-tier limit is reached

Requirement: Token introspection (RFC 7662)

The system SHALL determine whether a given access token is currently active (valid, not expired, not revoked). The endpoint SHALL return 200 OK for both active and inactive tokens — the active field in the response SHALL indicate validity. The caller SHALL hold a valid Bearer token with tokens:read scope.

Scenario: Active token introspection

  • WHEN a POST request to /token/introspect is received with a valid, non-expired, non-revoked token and the caller has tokens:read scope
  • THEN the system returns 200 OK with active: true and the token's claims (sub, client_id, scope, token_type, iat, exp)

Scenario: Expired or revoked token introspection

  • WHEN a POST request to /token/introspect is received with a token that is expired or has been revoked
  • THEN the system returns 200 OK with active: false and no other claims

Scenario: Insufficient scope for introspection

  • WHEN a POST request to /token/introspect is received with a valid Bearer token that does not have tokens:read scope
  • THEN the system returns 403 Forbidden with code: INSUFFICIENT_SCOPE

Requirement: Token revocation (RFC 7009)

The system SHALL invalidate a given access token immediately. Revoking an already-revoked or expired token SHALL be a successful, idempotent operation (RFC 7009 §2.1). Revoked token JTIs SHALL be stored in Redis with TTL equal to the token's remaining lifetime.

Scenario: Successful token revocation

  • WHEN a POST request to /token/revoke is received with a valid Bearer token and a token parameter containing a valid JWT
  • THEN the system adds the token's JTI to the Redis revocation list, and returns 200 OK with an empty body

Scenario: Revocation of already-revoked token is idempotent

  • WHEN a POST request to /token/revoke is received with a token that is already in the Redis revocation list
  • THEN the system returns 200 OK with an empty body (no error)

Scenario: Missing token parameter rejected

  • WHEN a POST request to /token/revoke is received with no token field in the body
  • THEN the system returns 400 Bad Request with code: VALIDATION_ERROR

Requirement: JWT claims structure

All issued JWTs SHALL contain the following claims: sub (agentId), client_id (agentId), scope (space-separated granted scopes), jti (UUID, unique per token), iat (issued-at Unix timestamp), exp (expiry Unix timestamp). Tokens SHALL be signed with RS256.

Scenario: JWT contains required claims

  • WHEN a token is issued via POST /token
  • THEN the decoded JWT payload contains sub, client_id, scope, jti, iat, and exp fields

Requirement: Rate limiting on token endpoints

The system SHALL enforce a rate limit of 100 requests per minute per client_id on all token endpoints.

Scenario: Rate limit exceeded on token endpoint

  • WHEN a client sends more than 100 requests to any token endpoint within a 60-second window
  • THEN the system returns 429 Too Many Requests with X-RateLimit-Limit, X-RateLimit-Remaining: 0, and X-RateLimit-Reset headers