Authenticating AI Agents with Keycloak: OAuth 2.0 for LLM Apps
Last updated: March 2026
Introduction
AI agents are no longer theoretical. LLM-powered applications now browse the web, call APIs, execute code, and interact with enterprise systems on behalf of users. Every one of those actions needs authentication and authorization.
The good news is that the OAuth 2.0 framework already has the building blocks for this. Keycloak, as a full-featured identity provider supporting single sign-on and fine-grained access control, is well suited for issuing and managing tokens that AI agents use to access protected resources.
This guide covers the authentication patterns emerging for AI agents: client credentials for agent identity, scoped access tokens for tool use, Model Context Protocol (MCP) server auth, human-in-the-loop consent flows, and audit trails for agent actions. We include working Python examples using LangChain with Keycloak-issued tokens.
If you have worked with machine-to-machine authentication before, much of this will feel familiar. The key difference is that AI agents often act on behalf of users and need constrained, auditable access.
Why AI Agents Need Formal Authentication
It is tempting to give an AI agent a long-lived API key and call it done. Here is why that falls apart:
Lack of scoping. API keys typically grant blanket access. An agent that only needs to read calendar events should not be able to delete contacts.
No user context. When an agent acts on behalf of a user, the downstream service needs to know which user, not just which agent.
No revocation granularity. If an agent is compromised, you need to revoke its access without affecting other agents or the user’s own access.
Audit gaps. Compliance requirements demand knowing exactly what an AI agent did, when, and on whose behalf. A shared API key makes this nearly impossible.
OAuth 2.0 addresses all of these with scoped tokens, token introspection, revocation endpoints, and claims-based identity. Keycloak adds audit logging and session management on top.
Pattern 1: Client Credentials for Agent Identity
The simplest pattern applies when an AI agent operates as its own entity, not on behalf of a specific user. Think background processing agents, data pipeline agents, or internal automation.
Keycloak Configuration
- Create a confidential client in Keycloak (e.g.,
ai-data-agent). - Enable Client Authentication (ON) and Service Accounts Enabled (ON).
- Disable all other authentication flows.
- Under Service Account Roles, assign only the roles this agent needs.
Use RBAC to define granular roles like data:read, data:write, and reports:generate rather than broad admin roles.
Python Implementation
import requests
class KeycloakAgentAuth:
"""Manages OAuth 2.0 client credentials tokens for an AI agent."""
def __init__(self, keycloak_url: str, realm: str,
client_id: str, client_secret: str):
self.token_url = (
f"{keycloak_url}/realms/{realm}"
f"/protocol/openid-connect/token"
)
self.client_id = client_id
self.client_secret = client_secret
self._token = None
def get_token(self, scope: str = "") -> str:
"""Obtain an access token using client credentials grant."""
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
if scope:
data["scope"] = scope
response = requests.post(self.token_url, data=data)
response.raise_for_status()
self._token = response.json()["access_token"]
return self._token
def auth_header(self) -> dict:
"""Return an Authorization header dict."""
if not self._token:
self.get_token()
return {"Authorization": f"Bearer {self._token}"}
# Usage
agent_auth = KeycloakAgentAuth(
keycloak_url="https://keycloak.example.com",
realm="ai-services",
client_id="ai-data-agent",
client_secret="agent-secret-here",
)
# Request a token with limited scope
token = agent_auth.get_token(scope="data:read")
# Use it to call a protected API
response = requests.get(
"https://api.example.com/v1/reports",
headers=agent_auth.auth_header(),
)
You can inspect the issued token with the JWT Token Analyzer to verify that only the requested scopes appear in the scope claim.
Pattern 2: Delegated Access (Agent Acting on Behalf of a User)
More commonly, an AI agent acts on behalf of a user. For example, an AI assistant reads a user’s emails, creates calendar events, or queries their CRM data. The agent needs a token that represents both the agent’s identity and the user’s authorization.
Token Exchange Flow
OAuth 2.0 Token Exchange (RFC 8693) lets a user’s token be exchanged for a constrained token that the agent can use:
def exchange_token_for_agent(
keycloak_url: str,
realm: str,
user_token: str,
agent_client_id: str,
agent_client_secret: str,
target_audience: str,
requested_scope: str = "openid",
) -> str:
"""
Exchange a user's token for a constrained agent token.
The resulting token carries the user's identity but is
scoped to only what the agent needs.
"""
token_url = (
f"{keycloak_url}/realms/{realm}"
f"/protocol/openid-connect/token"
)
response = requests.post(token_url, data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": user_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"client_id": agent_client_id,
"client_secret": agent_client_secret,
"audience": target_audience,
"scope": requested_scope,
"requested_token_type":
"urn:ietf:params:oauth:token-type:access_token",
})
response.raise_for_status()
return response.json()["access_token"]
Enable Token Exchange in Keycloak by adding the token-exchange permission to the agent’s client under Authorization > Permissions. See the Keycloak Token Exchange documentation for full setup details.
Scoped Access for Tool Use
When an AI agent uses tools (web search, code execution, database queries), each tool should receive a token scoped to just that tool’s needs:
class AgentToolManager:
"""Manages scoped tokens for different AI agent tools."""
TOOL_SCOPES = {
"web_search": "search:execute",
"database_query": "db:read",
"email_send": "email:send",
"calendar_read": "calendar:read calendar:list",
"file_upload": "storage:write",
}
def __init__(self, auth: KeycloakAgentAuth):
self.auth = auth
self._tool_tokens: dict[str, str] = {}
def get_tool_token(self, tool_name: str) -> str:
"""Get a scoped token for a specific tool."""
scope = self.TOOL_SCOPES.get(tool_name)
if not scope:
raise ValueError(f"Unknown tool: {tool_name}")
token = self.auth.get_token(scope=scope)
self._tool_tokens[tool_name] = token
return token
def execute_tool(self, tool_name: str,
endpoint: str, **kwargs) -> dict:
"""Execute a tool call with its scoped token."""
token = self.get_tool_token(tool_name)
response = requests.get(
endpoint,
headers={"Authorization": f"Bearer {token}"},
**kwargs,
)
response.raise_for_status()
return response.json()
Pattern 3: MCP Server Authentication
The Model Context Protocol (MCP) is an emerging standard for connecting LLMs to external tools and data sources. MCP servers expose capabilities that AI agents can discover and invoke. Securing these servers with OAuth 2.0 is essential.
We covered MCP server security in depth in our MCP + Keycloak guide. Here is a concise implementation:
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import httpx
app = FastAPI()
security = HTTPBearer()
KEYCLOAK_URL = "https://keycloak.example.com"
REALM = "ai-services"
JWKS_URL = f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/certs"
async def verify_agent_token(
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
"""
Verify the bearer token against Keycloak and return claims.
In production, use python-jose or PyJWT with cached JWKS.
"""
token = credentials.credentials
# Introspect the token with Keycloak
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{KEYCLOAK_URL}/realms/{REALM}"
f"/protocol/openid-connect/token/introspect",
data={
"token": token,
"client_id": "mcp-resource-server",
"client_secret": "mcp-server-secret",
},
)
token_info = resp.json()
if not token_info.get("active"):
raise HTTPException(status_code=401, detail="Invalid token")
return token_info
@app.post("/mcp/tools/search")
async def mcp_search_tool(
query: dict,
claims: dict = Depends(verify_agent_token),
):
"""MCP tool endpoint protected by Keycloak auth."""
# Verify the agent has the required scope
scopes = claims.get("scope", "").split()
if "search:execute" not in scopes:
raise HTTPException(
status_code=403,
detail="Insufficient scope for search tool",
)
agent_id = claims.get("azp", "unknown")
user_sub = claims.get("sub", "unknown")
# Log the agent action for audit
print(f"Agent {agent_id} (user {user_sub}) executing search: "
f"{query}")
return {"results": ["result1", "result2"]}
Pattern 4: Human-in-the-Loop Consent
For sensitive operations, an AI agent should not proceed without explicit human approval. OAuth 2.0 consent screens combined with Keycloak’s step-up authentication provide this:
class ConsentRequiredError(Exception):
"""Raised when an agent action requires human consent."""
def __init__(self, action: str, consent_url: str):
self.action = action
self.consent_url = consent_url
class HumanInTheLoopAgent:
"""Agent that requests human consent for sensitive actions."""
SENSITIVE_SCOPES = {"email:send", "payment:execute",
"data:delete", "admin:modify"}
def __init__(self, keycloak_url: str, realm: str,
client_id: str):
self.keycloak_url = keycloak_url
self.realm = realm
self.client_id = client_id
def request_action(self, action_scope: str,
user_session: str) -> str:
"""
Request permission for a sensitive action.
Returns a token if already consented, or raises
ConsentRequiredError with a URL for the user.
"""
if action_scope in self.SENSITIVE_SCOPES:
# Build an authorization URL that will prompt the user
auth_url = (
f"{self.keycloak_url}/realms/{self.realm}"
f"/protocol/openid-connect/auth"
f"?client_id={self.client_id}"
f"&response_type=code"
f"&scope=openid {action_scope}"
f"&prompt=consent"
f"&redirect_uri=https://app.example.com/agent/callback"
f"&state={user_session}"
)
raise ConsentRequiredError(
action=action_scope,
consent_url=auth_url,
)
# Non-sensitive: proceed with existing token
return self._get_service_token(action_scope)
def _get_service_token(self, scope: str) -> str:
resp = requests.post(
f"{self.keycloak_url}/realms/{self.realm}"
f"/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": "secret",
"scope": scope,
},
)
resp.raise_for_status()
return resp.json()["access_token"]
For step-up authentication patterns (requiring MFA before the agent can proceed), see our step-up authentication guide and multi-factor authentication features.
Pattern 5: LangChain Integration
Here is a practical example integrating Keycloak authentication with LangChain’s tool-calling framework:
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
# Initialize Keycloak auth for the agent
agent_auth = KeycloakAgentAuth(
keycloak_url="https://keycloak.example.com",
realm="ai-services",
client_id="langchain-agent",
client_secret="langchain-secret",
)
tool_manager = AgentToolManager(agent_auth)
@tool
def search_company_data(query: str) -> str:
"""Search internal company data with proper authentication."""
result = tool_manager.execute_tool(
tool_name="database_query",
endpoint="https://api.example.com/v1/search",
params={"q": query},
)
return str(result)
@tool
def send_email_notification(to: str, subject: str,
body: str) -> str:
"""Send an email notification. Requires elevated permissions."""
token = tool_manager.get_tool_token("email_send")
response = requests.post(
"https://api.example.com/v1/email/send",
headers={"Authorization": f"Bearer {token}"},
json={"to": to, "subject": subject, "body": body},
)
response.raise_for_status()
return f"Email sent to {to}"
# Create the agent with authenticated tools
llm = ChatOpenAI(model="gpt-4o")
tools = [search_company_data, send_email_notification]
llm_with_tools = llm.bind_tools(tools)
Audit Trail for AI Agent Actions
Every action an AI agent takes should be auditable. Keycloak’s event system captures token issuance, exchange, and introspection events. For the actions themselves, implement structured logging:
import json
import logging
from datetime import datetime, timezone
logger = logging.getLogger("agent_audit")
def log_agent_action(
agent_client_id: str,
user_sub: str | None,
action: str,
resource: str,
scope_used: str,
result: str,
metadata: dict | None = None,
):
"""Log an AI agent action for audit purposes."""
audit_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": "agent_action",
"agent_id": agent_client_id,
"user_sub": user_sub,
"action": action,
"resource": resource,
"scope": scope_used,
"result": result,
"metadata": metadata or {},
}
logger.info(json.dumps(audit_entry))
Forward these logs to your SIEM alongside Keycloak events. We have guides on forwarding Keycloak events to SIEM and integrating security logs via syslog that cover this in detail.
On Skycloak, audit logs are available out of the box through the audit logs feature, and you can configure webhook forwarding to any SIEM platform.
Security Best Practices for AI Agent Auth
| Practice | Why It Matters |
|---|---|
| Short-lived tokens (5-15 min) | Limits the window if a token is leaked |
| Narrow scopes per tool | Prevents an agent from exceeding its intended access |
| Token exchange over shared secrets | Maintains user context and enables revocation |
| Audience restriction | Ensures tokens are only valid for intended services |
| Refresh token rotation | Detects token theft via rotation |
| Separate clients per agent type | Isolates blast radius and simplifies audit |
| PKCE for interactive flows | Required for browser-based consent (see PKCE guide) |
For a deeper dive into JWT security practices relevant to agent tokens, see our upcoming guide on JWT best practices.
Conclusion
AI agent authentication follows the same OAuth 2.0 principles as any other client, with extra emphasis on scoping, delegation, and auditability. Keycloak provides the complete toolkit: client credentials for agent identity, token exchange for delegated access, fine-grained scopes for tool-level authorization, and event logging for compliance.
The patterns covered here, client credentials, token exchange, MCP server auth, human-in-the-loop consent, and LangChain integration, give you a foundation for securing AI agents in production. As the ecosystem matures (particularly around MCP and agent-to-agent communication), these OAuth 2.0 primitives will remain the foundation.
Looking for a managed Keycloak instance to power your AI agent authentication? See Skycloak pricing and have a production-ready identity provider running in minutes.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.