Skip to main content

OAuth 2.1 Discovery

STOA implements OAuth 2.1 discovery standards so AI agents and OAuth clients can automatically discover authentication endpoints and obtain tokens β€” without hardcoding Keycloak URLs.

Why Discovery Matters​

Without discovery, every MCP client would need to know:

  • The Keycloak URL
  • The realm name
  • The token endpoint path
  • The supported grant types

With discovery, the client only needs the gateway URL (mcp.<YOUR_DOMAIN>) and can discover everything else automatically.


Protected Resource Metadata (RFC 9728)​

GET /.well-known/oauth-protected-resource​

Tells OAuth clients that this resource is protected and where to authenticate.

curl "https://mcp.<YOUR_DOMAIN>/.well-known/oauth-protected-resource"

Response:

{
"resource": "https://mcp.<YOUR_DOMAIN>",
"authorization_servers": [
"https://auth.<YOUR_DOMAIN>/realms/stoa"
],
"scopes_supported": [
"openid",
"profile",
"email",
"stoa:read",
"stoa:write",
"stoa:admin"
],
"bearer_methods_supported": ["header"]
}
FieldDescription
resourceThe protected resource identifier (gateway URL)
authorization_serversList of authorization servers that can issue tokens
scopes_supportedOAuth scopes the resource understands
bearer_methods_supportedHow to present tokens (header = Authorization header)

Authorization Server Metadata (RFC 8414)​

GET /.well-known/oauth-authorization-server​

Returns authorization server metadata. Token and registration endpoints point to the gateway (not Keycloak directly), enabling the gateway to proxy these requests.

curl "https://mcp.<YOUR_DOMAIN>/.well-known/oauth-authorization-server"

Response:

{
"issuer": "https://auth.<YOUR_DOMAIN>/realms/stoa",
"authorization_endpoint": "https://auth.<YOUR_DOMAIN>/realms/stoa/protocol/openid-connect/auth",
"token_endpoint": "https://mcp.<YOUR_DOMAIN>/oauth/token",
"registration_endpoint": "https://mcp.<YOUR_DOMAIN>/oauth/register",
"jwks_uri": "https://auth.<YOUR_DOMAIN>/realms/stoa/protocol/openid-connect/certs",
"scopes_supported": [
"openid", "profile", "email",
"stoa:read", "stoa:write", "stoa:admin"
],
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
],
"token_endpoint_auth_methods_supported": [
"none",
"client_secret_basic",
"client_secret_post"
],
"code_challenge_methods_supported": ["S256"],
"subject_types_supported": ["public"]
}

Key points:

  • token_endpoint points to the gateway (/oauth/token), not Keycloak directly
  • registration_endpoint points to the gateway (/oauth/register) for DCR
  • authorization_endpoint points to Keycloak (user-facing login page)
  • PKCE S256 is required for authorization code flows

OpenID Connect Discovery​

GET /.well-known/openid-configuration​

Proxies Keycloak's OIDC discovery document, overriding token and registration endpoints to route through the gateway.

curl "https://mcp.<YOUR_DOMAIN>/.well-known/openid-configuration"

The response is Keycloak's standard OIDC discovery with two overrides:

  • token_endpoint β†’ https://mcp.<YOUR_DOMAIN>/oauth/token
  • registration_endpoint β†’ https://mcp.<YOUR_DOMAIN>/oauth/register
StatusMeaning
200 OKDiscovery document returned
502 Bad GatewayKeycloak unreachable
503 Service UnavailableKeycloak not configured

Token Proxy​

POST /oauth/token​

Transparent proxy to Keycloak's token endpoint. The gateway forwards the request body as-is and returns the Keycloak response.

Client Credentials Grant:

curl -X POST "https://mcp.<YOUR_DOMAIN>/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=my-app" \
-d "client_secret=my-secret"

Authorization Code Grant (with PKCE):

curl -X POST "https://mcp.<YOUR_DOMAIN>/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "client_id=my-app" \
-d "code_verifier=PKCE_VERIFIER" \
-d "redirect_uri=https://myapp.example.com/callback"

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh-token-xyz",
"scope": "openid profile email stoa:read"
}
StatusMeaning
200 OKToken issued
400 Bad RequestInvalid grant or parameters
401 UnauthorizedInvalid client credentials
502 Bad GatewayKeycloak unreachable

Dynamic Client Registration (DCR)​

POST /oauth/register​

Proxy to Keycloak's DCR endpoint. After registration, the gateway automatically patches the client to be public (no client_secret) with PKCE S256 support β€” required for AI agents like Claude.ai.

curl -X POST "https://mcp.<YOUR_DOMAIN>/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "claude-mcp-connector",
"application_type": "web",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"redirect_uris": ["https://claude.ai/oauth/callback"]
}'

Response (201 Created):

{
"client_id": "new-client-abc123",
"client_name": "claude-mcp-connector",
"application_type": "web",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"registration_access_token": "rat-xyz",
"registration_client_uri": "https://auth.<YOUR_DOMAIN>/..."
}

Post-Registration PKCE Patch​

After successful registration, the gateway automatically:

  1. Obtains an admin token from Keycloak
  2. Finds the newly created client
  3. Patches it to set publicClient: true and pkce.code.challenge.method: S256

This ensures AI agents can authenticate using the authorization code flow with PKCE, without needing a client secret.

StatusMeaning
201 CreatedClient registered
400 Bad RequestInvalid registration parameters
403 ForbiddenDCR disabled in Keycloak
502 Bad GatewayKeycloak unreachable

AI Agent Integration Example​

Here's how an AI agent (e.g., Claude.ai) discovers and authenticates with STOA:

  1. Discover: GET /.well-known/oauth-protected-resource β†’ finds authorization server
  2. Metadata: GET /.well-known/oauth-authorization-server β†’ finds token endpoint, supported flows
  3. Register (first time): POST /oauth/register β†’ gets client_id
  4. Authorize: Redirect user to Keycloak login β†’ get authorization code
  5. Token: POST /oauth/token with code + PKCE verifier β†’ get access token
  6. Call tools: POST /mcp/tools/call with Authorization: Bearer <token>