Skip to Content
ProtocolsOAuth 2.1

OAuth 2.1 Implementation

AgentTrust ID’s OAuth 2.1 authorization server with mandatory PKCE, supporting MCP compliance and agent authentication.

Overview

AgentTrust ID implements OAuth 2.1 to authenticate MCP clients and issue AgentTrust ID JWT tokens. Key properties:

  • PKCE required — S256 code challenge method is mandatory (no plain)
  • Authorization code flow only — no implicit or client credentials grants
  • AgentTrust ID JWT integration — OAuth token exchange issues the same JWT tokens used across AgentTrust ID
  • Dynamic client registration — RFC 7591 support for automated MCP client onboarding

PKCE Flow

OAuth 2.1 PKCE flow The MCP Client generates a code_verifier and a challenge equal to SHA256 of the verifier. It calls GET /oauth/authorize with response_type=code, client_id, code_challenge, code_challenge_method=S256, redirect_uri, agent_id, and org_id. The OAuth Server returns a 302 redirect with a code. The client posts to /oauth/token with grant_type=authorization_code, code, code_verifier, client_id, and redirect_uri, and the server returns an access_token JWT. MCP Client AgentTrust ID OAuth Server 1. Generate code_verifier challenge = SHA256(verifier) GET /oauth/authorize ?response_type=code&client_id=...&code_challenge=<challenge> &code_challenge_method=S256&redirect_uri=...&agent_id=...&org_id=... C: 302 redirect with code --> 302 redirect with ?code=... POST /oauth/token grant_type=authorization_code&code=... &code_verifier=<verifier>&client_id=...&redirect_uri=... C: access_token --> { "access_token": "<jwt>" } Auth codes expire in 10 minutes · access tokens in 60 minutes

API Reference

GET /.well-known/oauth-authorization-server — Server Metadata

RFC 8414 discovery endpoint. Returns server capabilities.

curl http://localhost:8080/.well-known/oauth-authorization-server

Response:

{ "issuer": "https://api.agenttrust.id/", "authorization_endpoint": "https://api.agenttrust.id/oauth/authorize", "token_endpoint": "https://api.agenttrust.id/oauth/token", "registration_endpoint": "https://api.agenttrust.id/oauth/register", "scopes_supported": ["read", "write", "admin", "mcp:tools", "mcp:resources", "mcp:prompts"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code"], "token_endpoint_auth_methods_supported": ["none", "client_secret_post"], "code_challenge_methods_supported": ["S256"] }

Auth methods: The token endpoint supports none (public clients relying on PKCE only) and client_secret_post (secret sent as a form parameter in the request body). HTTP Basic Auth (client_secret_basic) is not implemented — the token handler reads client_id exclusively from the request body (form or JSON), not from the Authorization header. See internal/oauth2/handler.go Token().

GET /oauth/authorize — Authorization Endpoint

Validates params and redirects with an authorization code.

ParameterRequiredDescription
response_typeyesMust be code
client_idyesRegistered client ID
redirect_uriyesMust match registered URI
code_challengeyesS256 PKCE challenge
code_challenge_methodyesMust be S256
agent_idyesAgentTrust ID agent requesting authorization
org_idyesOrganization scope
scopenoSpace-separated scopes
statenoCSRF protection value

POST /oauth/token — Token Exchange

Accepts application/x-www-form-urlencoded or application/json.

curl -X POST http://localhost:8080/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code\ &code=auth-code-here\ &redirect_uri=http://localhost:3000/callback\ &client_id=client-id-here\ &code_verifier=original-verifier"

Response:

{ "access_token": "eyJhbGciOiJFZERTQSIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "read write mcp:tools" }

Note: Access tokens are JWTs signed with EdDSA (Ed25519), not HS256 or RS256.

Response includes Cache-Control: no-store and Pragma: no-cache headers.

POST /oauth/register — Dynamic Client Registration

RFC 7591 endpoint. Requires X-Org-ID header.

curl -X POST http://localhost:8080/oauth/register \ -H "X-Org-ID: org-uuid" \ -H "Content-Type: application/json" \ -d '{ "client_name": "my-mcp-client", "redirect_uris": ["http://localhost:3000/callback"], "grant_types": ["authorization_code"], "scope": ["read", "mcp:tools"] }'

Response (201):

{ "client_id": "generated-hex-id", "client_secret": "generated-hex-secret", "client_name": "my-mcp-client", "redirect_uris": ["http://localhost:3000/callback"], "grant_types": ["authorization_code"], "scope": ["read", "mcp:tools"] }

client_secret is only returned at registration (stored as SHA-256 hash). Public clients omit the secret.

Token, Secret, and Code Sizes

All random values are generated via crypto/rand and hex-encoded. Source: internal/oauth2/server.go randomHex().

ValueBytesHex charsExample
Client ID1632a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
Client secret326464-char hex string
Authorization code64128128-char hex string

AgentTrust ID JWT Integration

The OAuth token endpoint issues the same JWT tokens used by AgentTrust ID’s internal auth service. When an MCP client exchanges an authorization code, it receives an AgentTrust ID JWT that works across all AgentTrust ID services:

OAuth code exchange --> AgentTrust ID TokenService.IssueToken() --> Standard AgentTrust ID JWT

This means OAuth-authenticated MCP clients get the same authorization checks (Guardian, policies, revocation) as API-key-authenticated agents.

agent_id and org_id Flow

During token exchange, agent_id and org_id are not taken from the token request body. They come from the stored authorization code record that was created during the /oauth/authorize step.

1. /oauth/authorize?agent_id=...&org_id=... --> Stored in oauth2_auth_codes row alongside the code 2. POST /oauth/token (body: grant_type, code, client_id, redirect_uri, code_verifier) --> ExchangeToken() looks up the auth code row --> Reads ac.AgentID and ac.OrgID from that row --> Passes ac.AgentID and ac.Scope to TokenService.IssueToken()

The token request itself only carries client_id for client validation, not agent identity. This prevents a malicious token request from overriding the agent_id that was authorized. Source: internal/oauth2/server.go ExchangeToken().

AgentTrust ID Scope Integration

OAuth scopes flow into the AgentTrust ID authorization system through two paths:

Path 1: AgentTrust ID OAuth (standard agents)

The OAuth token endpoint calls crypto.TokenService.IssueToken() with the scopes from the stored authorization code. These scopes are embedded in the AgentTrust ID JWT claims and become the agent’s authorized scope for all downstream Guardian policy checks.

/oauth/authorize (scope=read+mcp:tools) --> stored in oauth2_auth_codes.scope /oauth/token --> ExchangeToken() reads ac.Scope --> TokenService.IssueToken(AgentID, Scope, TTL) --> JWT claims include scope array

Path 2: MCP OAuth (CIMD-based clients)

The MCP auth handler (internal/mcp/auth_handler.go Token()) generates an MCP-specific bearer token and stores its scope in Redis via ScopeEnforcer.StoreTokenScope(). During subsequent MCP proxy requests, ScopeEnforcer.GetTokenScope() retrieves the token’s scope, and ScopeEnforcer.CheckScopeCeiling() validates that each requested action falls within the scope ceiling.

POST /mcp/oauth/token (scope=mcp:tool_call) --> ScopeEnforcer.StoreTokenScope(token, scope, 1h) [Redis key: mcp_token_scope:<token>] MCP proxy request with Bearer token --> ScopeEnforcer.GetTokenScope(token) [reads from Redis] --> ScopeEnforcer.CheckScopeCeiling(ceiling, action) [enforces scope boundary]

These scopes become the AgentTrust ID session scope_ceiling — the maximum set of permissions for the session. See AGENTTRUST.md for the session scope-enforcement model.

Security Considerations

  • PKCE is mandatory — requests without code_challenge are rejected
  • Only S256 — plain code challenge method is not supported
  • Single-use codes — authorization codes are marked as used after exchange
  • Redirect URI validation — must exactly match a registered URI
  • Client secrets — SHA-256 hashed, never stored in plain text
  • Confidential vs. public clients — confidential clients get a secret; public clients use PKCE only
  • Confidential client secret validation — for confidential clients, ExchangeToken() validates the presented client_secret against the stored SHA-256 hash with a constant-time comparison, in addition to client_id match, redirect_uri match, code expiry, single-use, and PKCE. Public clients (no secret) rely on PKCE. Source: internal/oauth2/server.go ExchangeToken() and internal/oauth2/handler.go Token()
Last updated on