Authorization
Understanding RFC 8693 token exchange for delegated access
Keycard implements RFC 8693 OAuth 2.0 Token Exchange to enable secure, delegated access to protected resources. This allows applications to exchange one type of token for another, enabling AI agents and MCP servers to access resources on behalf of authenticated users.
What is Token Exchange?
Section titled “What is Token Exchange?”Token exchange is the process of converting one token into another to access different resources while maintaining the user’s identity and authorization context.
The Problem
Section titled “The Problem”Consider an AI agent that needs to:
- Authenticate the user
- Access the user’s GitHub repositories
- Access the user’s Google Calendar
- Access the user’s Slack workspace
Without token exchange, you would need to:
- Build custom OAuth flows for each service
- Manage multiple token lifecycles
- Handle refresh logic for each provider
- Store credentials securely for each integration
The Solution
Section titled “The Solution”With Keycard’s token exchange:
User Token (Identity) → Exchange → GitHub Token → Exchange → Google Token → Exchange → Slack TokenOne authentication flow, automatic token exchange for all resources.
RFC 8693 Standard
Section titled “RFC 8693 Standard”Keycard implements RFC 8693: OAuth 2.0 Token Exchange, an IETF standard for exchanging OAuth tokens.
Key Concepts
Section titled “Key Concepts”| Subject Token | The input token (user’s identity token from Keycard) |
| Requested Token | The output token (resource-specific access token) |
| Resource | The target API or service requiring access |
| Scope | The permissions requested for the resource |
| Actor Token (optional) | Token representing a service acting on behalf of the user |
Grant Type
Section titled “Grant Type”urn:ietf:params:oauth:grant-type:token-exchangeThis special grant type tells the authorization server to perform token exchange rather than standard OAuth flows.
How Token Exchange Works
Section titled “How Token Exchange Works”High-Level Flow
Section titled “High-Level Flow”sequenceDiagram
participant User
participant Agent as AI Agent/MCP Server
participant KC as Keycard
participant Resource as External Resource
User->>Agent: Make request
Agent->>KC: Exchange token for GitHub access
KC->>Resource: Request user's GitHub token
Resource->>User: Authorize access
User->>Resource: Grant permission
Resource->>KC: GitHub access token
KC->>Agent: Exchanged token
Agent->>Resource: Access GitHub API
Resource->>Agent: API response
Agent->>User: Result
Step-by-Step Process
Section titled “Step-by-Step Process”-
User Authenticates
User authenticates to your application via Keycard:
// User logs in, application receives Keycard JWTconst userToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...";This token represents the user’s identity and their authorization for your application.
-
Application Requests Resource Access
Your application needs to access a protected resource (e.g., GitHub API):
// Application declares dependency on GitHubconst dependencies = {"https://api.github.com": ["repo", "user:email"],}; -
Token Exchange Request
Application calls Keycard’s token exchange endpoint:
POST https://z-abc123.keycard.cloud/oauth/tokenContent-Type: application/x-www-form-urlencodedgrant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...&subject_token_type=urn:ietf:params:oauth:token-type:access_token&resource=https://api.github.com&scope=repo user:email&client_id=<your-application-client-id>&client_secret=<your-application-client-secret>Parameters:
grant_typeToken exchange grant type (RFC 8693) subject_tokenUser’s Keycard JWT (input token) subject_token_typeType of input token (access_token) resourceTarget resource identifier (GitHub API) scopeRequested permissions client_id/client_secretApplication credentials -
Keycard Processes Exchange
Keycard validates and processes the request:
- Validate subject token: Verify JWT signature, expiration, audience
- Check authorization: Ensure user authorized app to access GitHub
- Check dependencies: Verify app declared GitHub as a dependency
- Retrieve or fetch token: Get cached GitHub token or initiate OAuth flow
- Issue exchanged token: Return resource-specific access token
-
Token Exchange Response
Keycard returns the exchanged token:
{"access_token": "gho_abc123def456...","issued_token_type": "urn:ietf:params:oauth:token-type:access_token","token_type": "Bearer","expires_in": 3600,"scope": "repo user:email"} -
Access Protected Resource
Application uses the exchanged token:
const response = await fetch("https://api.github.com/user/repos", {headers: {Authorization: `Bearer ${exchangedToken.access_token}`,Accept: "application/vnd.github.v3+json",},});
Implementation Examples
Section titled “Implementation Examples”TypeScript SDK
Section titled “TypeScript SDK”import { STSClient } from "./services/sts-client";import { requireBearerAuth } from "@keycardlabs/mcp-oauth/server/auth/middleware/bearerAuth";
// Protect your endpointapp.post( "/mcp", requireBearerAuth({ requiredScopes: [] }), async (req, res) => { // User's Keycard JWT available via middleware const { authInfo } = req;
// Create STS client const stsClient = new STSClient(process.env.KEYCARD_STS_ISSUER_URL);
// Exchange token for GitHub access const githubToken = await stsClient.exchange( authInfo, "https://api.github.com", ["repo", "user:email"], );
// Use exchanged token const repos = await fetch("https://api.github.com/user/repos", { headers: { Authorization: `Bearer ${githubToken.access_token}` }, });
res.json(await repos.json()); },);Python SDK
Section titled “Python SDK”from keycardai.mcp.server.auth import AuthProvider, AccessContext
auth = AuthProvider( mcp_server_name="GitHub MCP Server", base_url="https://z-abc123.keycard.cloud")
@mcp.tool(name="list_repos")@auth.grant("https://api.github.com")async def list_repos(ctx, access_context: AccessContext): # Check for errors if access_context.has_errors(): return {"error": "GitHub access not authorized"}
# Token exchange handled automatically by decorator github_token = access_context.access_token
# Use exchanged token async with httpx.AsyncClient() as client: response = await client.get( "https://api.github.com/user/repos", headers={"Authorization": f"Bearer {github_token}"} ) return response.json()STS Client Implementation
Section titled “STS Client Implementation”For frameworks without native Keycard SDK support:
export class STSClient { constructor(private stsBaseUrl: string) {}
async exchange( authInfo: AuthInfo, resource: string, scopes?: string[], ): Promise<{ access_token: string }> { const params = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", subject_token: authInfo.token, subject_token_type: "urn:ietf:params:oauth:token-type:access_token", resource: resource, });
if (scopes?.length) { params.set("scope", scopes.join(" ")); }
const response = await fetch(`${this.stsBaseUrl}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: params.toString(), });
if (!response.ok) { throw new Error(`Token exchange failed: ${response.statusText}`); }
return await response.json(); }}Token Types
Section titled “Token Types”Subject Token (Input)
Section titled “Subject Token (Input)”The token being exchanged - typically a Keycard JWT representing the user:
{ "iss": "https://z-abc123.keycard.cloud", "sub": "user-123", "aud": "https://mcp.example.com", "scope": "mcp:tools", "exp": 1234567890, "iat": 1234564290}Token Type: urn:ietf:params:oauth:token-type:access_token
Issued Token (Output)
Section titled “Issued Token (Output)”The exchanged token for accessing the specific resource:
{ "access_token": "gho_abc123def456...", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": "repo user:email", "refresh_token": "ghr_xyz789..."}Token Type: urn:ietf:params:oauth:token-type:access_token or urn:ietf:params:oauth:token-type:refresh_token
Transitive Dependencies
Section titled “Transitive Dependencies”Keycard supports transitive token exchange - applications can exchange tokens on behalf of other applications.
Example: AI Agent → MCP Server → External API
Section titled “Example: AI Agent → MCP Server → External API”User └─> Authenticates to AI Agent └─> AI Agent calls MCP Server (with user token) └─> MCP Server exchanges for GitHub token └─> MCP Server accesses GitHub APIFlow Diagram
Section titled “Flow Diagram”sequenceDiagram
participant User
participant Agent as AI Agent
participant MCP as MCP Server
participant KC as Keycard
participant GitHub
User->>Agent: Request with auth
Agent->>KC: Exchange user token for MCP token
KC->>Agent: MCP access token
Agent->>MCP: Call tool with MCP token
MCP->>KC: Exchange MCP token for GitHub token
KC->>GitHub: Request user's GitHub access
GitHub->>KC: GitHub token
KC->>MCP: GitHub access token
MCP->>GitHub: Access user's repos
GitHub->>MCP: Repo data
MCP->>Agent: Tool result
Agent->>User: Response
Automatic Orchestration
Section titled “Automatic Orchestration”Keycard SDKs handle transitive exchanges automatically:
# AI Agent code@agent.tool(name="get_github_repos")@auth.grant("https://mcp.example.com") # MCP serverasync def get_repos(ctx, access_context): # Token exchange to MCP server handled automatically mcp_token = access_context.access_token
# Call MCP server result = await mcp_client.call_tool("list_repos", token=mcp_token) return result
# MCP Server code (separate service)@mcp.tool(name="list_repos")@auth.grant("https://api.github.com") # GitHub APIasync def list_repos(ctx, access_context): # Token exchange to GitHub handled automatically github_token = access_context.access_token
# Access GitHub repos = await github_client.get_repos(token=github_token) return reposUser authenticates once, two automatic token exchanges occur.
Caching & Performance
Section titled “Caching & Performance”Token Caching
Section titled “Token Caching”Keycard caches exchanged tokens to improve performance:
- Duration: Based on token expiration (typically 1 hour)
- Scope: Per user + resource + scope combination
- Invalidation: Automatic on token expiration or user revocation
Cache Benefits
Section titled “Cache Benefits”| Without Caching | With Caching |
|---|---|
| OAuth flow on every request | OAuth flow only on cache miss |
| High latency (redirects + API calls) | Low latency (cache lookup) |
| Rate limit pressure on providers | Reduced API calls |
| Poor user experience | Seamless experience |
When Tokens Are Refreshed
Section titled “When Tokens Are Refreshed”- Token expires (1 hour default)
- User revokes access
- Application scopes change
- Resource provider rotates keys
SDK Caching
Section titled “SDK Caching”Keycard SDKs implement intelligent caching:
// First call: Full OAuth flow + token exchangeconst token1 = await exchangeToken(userToken, "github", ["repo"]);// Subsequent calls within 1 hour: Cachedconst token2 = await exchangeToken(userToken, "github", ["repo"]); // Fast!Security Considerations
Section titled “Security Considerations”-
Token Validation
Always validate exchanged tokens before use: -
Verify token signature
Use the resource provider’s JWKS endpoint to validate JWT signatures. -
Check expiration
Validateexpclaim before using the token. Refresh if expired. -
Validate audience
Ensure theaudclaim matches your application or the intended resource. -
Check scope
Verify the token has the required scopes for the operation. -
Handle errors gracefully
Token exchange can fail (revoked access, expired tokens, network issues). Implement retries and error handling.
Security Best Practices
Section titled “Security Best Practices”| Practice | Why | How |
|---|---|---|
| Least Privilege | Minimize attack surface | Request only required scopes |
| Short Lifetimes | Limit token exposure window | Use 1-hour access tokens |
| Secure Storage | Prevent token theft | Never log or persist exchanged tokens |
| Scope Validation | Enforce authorization | Check token scopes before API calls |
| Audit Logging | Detect suspicious activity | Log all token exchange events |
Token Binding
Section titled “Token Binding”Exchanged tokens are bound to:
- User: Can only be used for the original user’s resources
- Application: Can only be used by the requesting application
- Resource: Can only access the specified resource
This prevents token reuse attacks and unauthorized access.
Error Handling
Section titled “Error Handling”Common Errors
Section titled “Common Errors”invalid_grant
Section titled “invalid_grant”Cause: Subject token is invalid, expired, or revoked
Solution:
- Refresh the subject token
- Re-authenticate the user
- Check token expiration
try { const token = await exchangeToken(userToken, resource, scopes);} catch (error) { if (error.error === "invalid_grant") { // Re-authenticate user redirectToLogin(); }}unauthorized_client
Section titled “unauthorized_client”Cause: Application not authorized to access resource
Solution:
- Verify application has declared dependency on resource
- Check user has authorized application for resource
- Verify application credentials are correct
invalid_scope
Section titled “invalid_scope”Cause: Requested scopes not available or not authorized
Solution:
- Request only available scopes
- Check resource supports requested scopes
- User may need to re-authorize with additional scopes
server_error
Section titled “server_error”Cause: Temporary issue with Keycard or resource provider
Solution:
- Implement exponential backoff
- Retry the request
- Fall back to cached tokens if available
Comparison with Other Patterns
Section titled “Comparison with Other Patterns”vs. Direct OAuth
Section titled “vs. Direct OAuth”| Aspect | Direct OAuth | Token Exchange |
|---|---|---|
| Setup | Per-resource implementation | Single Keycard integration |
| Token Management | Manual refresh per provider | Automatic via Keycard |
| User Experience | Multiple authorization prompts | Single authorization |
| Security | Manage secrets per provider | Centralized secret management |
| Audit | Separate logs per provider | Unified audit trail |
vs. API Keys
Section titled “vs. API Keys”| Aspect | API Keys | Token Exchange |
|---|---|---|
| User Context | No user identity | Maintains user identity |
| Revocation | Manual rotation | Instant revocation |
| Expiration | Often permanent | Short-lived (1 hour) |
| Scope Control | All-or-nothing | Fine-grained scopes |
| Delegation | Not possible | Native support |