OAuth 2.0 Authentication Flow
This guide explains the complete OAuth 2.0 authentication flow used to authorize AI agents to access patient health records through the MCP Server.
Overview
The MCP Server implements OAuth 2.0 with the following features:
- Dynamic Client Registration
- Authorization Code Grant with PKCE
- Refresh Token Rotation
- Token Revocation
- OAuth Discovery
Step-by-Step Guide
Step 1: OAuth Discovery (Optional but Recommended)
Discover OAuth endpoints automatically instead of hardcoding URLs.
Request:
GET /.well-known/oauth-authorization-server
Response:
{
"issuer": "https://api.healthex.io",
"authorization_endpoint": "https://api.healthex.io/oauth/authorize",
"token_endpoint": "https://api.healthex.io/oauth/token",
"revocation_endpoint": "https://api.healthex.io/oauth/revoke",
"registration_endpoint": "https://api.healthex.io/oauth/register",
"scopes_supported": ["patient/*.read", "offline_access"],
"code_challenge_methods_supported": ["S256"]
}
Why Discovery?
- Future-proof against endpoint changes
- Automatically discover supported features
- Standard OAuth 2.0 best practice
Save these URLs:
registration_endpoint- For Step 2 (client registration)authorization_endpoint- For Step 3 (authorization request)token_endpoint- For Step 7 (token exchange)
Step 2: Client Registration
Register your AI agent to obtain credentials.
Request:
POST /oauth/register
Content-Type: application/json
{
"client_name": "Claude AI Integration",
"redirect_uris": ["https://claude.ai/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "patient/*.read offline_access"
}
Response:
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_id_issued_at": 1698765432,
"client_secret_expires_at": 0,
"redirect_uris": ["https://claude.ai/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "patient/*.read offline_access"
}
Key Points:
token_endpoint_auth_method: "none"indicates a public client (requires PKCE)offline_accessscope enables refresh tokens- Save the
client_idfor subsequent requests
Step 3: Generate PKCE Parameters
PKCE (Proof Key for Code Exchange) prevents authorization code interception.
Generate Code Verifier:
// Generate 128 bytes of random data
const codeVerifier = base64URLEncode(crypto.randomBytes(128))
// Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Compute Code Challenge:
// SHA256 hash of code verifier
const hash = crypto.createHash('sha256').update(codeVerifier).digest()
const codeChallenge = base64URLEncode(hash)
// Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Code Challenge Method:
code_challenge_method = "S256"
Step 4: Authorization Request
Redirect the user to the authorization endpoint.
Request:
GET /oauth/authorize?
response_type=code&
client_id=550e8400-e29b-41d4-a716-446655440000&
redirect_uri=https://claude.ai/oauth/callback&
scope=patient/*.read offline_access&
state=random_state_string_xyz&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Parameters:
| Parameter | Required | Description |
|---|---|---|
response_type | ✅ | Must be code |
client_id | ✅ | From registration |
redirect_uri | ✅ | Must match registered URI |
scope | ✅ | Space-separated scopes |
state | ✅ | Random string for CSRF protection |
code_challenge | ✅ | SHA256 hash of code_verifier |
code_challenge_method | ✅ | Must be S256 |
Response:
The server redirects to the HealthEx consent page:
https://app.healthex.io/#/patient-consent/{projectId}/enrollment/link?oauth_state=base64_params
Step 5: Patient Consent
The patient:
- Logs in or creates a HealthEx account
- Reviews what data the AI agent is requesting
- Grants or denies consent
- Waits while their health records are fetched and processed
During this step, the system:
- Retrieves patient records from connected EHRs
- Processes records for search
The authorization code is issued only after this process completes. The patient will see a loading screen during processing.
Step 6: Authorization Code Issued
After consent and data processing, the patient is redirected back to your application:
Redirect:
https://claude.ai/oauth/callback?
code=abc123def456ghi789&
state=random_state_string_xyz
Validate:
// Verify state matches the one you sent
if (receivedState !== sentState) {
throw new Error('CSRF attack detected')
}
Step 7: Token Exchange
Exchange the authorization code for access and refresh tokens.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=abc123def456ghi789&
redirect_uri=https://claude.ai/oauth/callback&
client_id=550e8400-e29b-41d4-a716-446655440000&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | ✅ | Must be authorization_code |
code | ✅ | Authorization code from redirect |
redirect_uri | ✅ | Must match authorization request |
client_id | ✅ | Your client ID |
code_verifier | ✅ | Original code_verifier (not hash!) |
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"scope": "patient/*.read offline_access"
}
Server Validation:
The server verifies:
- Authorization code is valid and not expired
redirect_urimatches the stored valueclient_idmatches the code owner- PKCE verification:
SHA256(code_verifier) === stored_code_challenge
If any check fails, an error is returned.
Step 8: Using the Access Token
Include the access token in all MCP requests.
Example:
POST /mcp
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search",
"arguments": {
"query": "diabetes medications"
}
}
}
Token Claims:
The JWT access token contains:
{
"sub": "patient-id",
"iss": "https://api.healthex.io",
"aud": "healthex.io",
"exp": 1698769032,
"iat": 1698765432,
"scope": "patient/*.read",
"organizationId": "org-id",
"firstName": "John",
"lastName": "Doe"
}
Step 9: Refresh Token Flow
When the access token expires, use the refresh token to get a new one.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6&
client_id=550e8400-e29b-41d4-a716-446655440000
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4",
"scope": "patient/*.read offline_access"
}
Important:
- A new refresh token is issued (token rotation)
- The old refresh token is invalidated
- Always save the new refresh token
Step 10: Token Revocation
Revoke tokens when the AI agent disconnects or the patient revokes consent.
Request:
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6&
token_type_hint=refresh_token&
client_id=550e8400-e29b-41d4-a716-446655440000
Response:
{
"success": true
}
Error Handling
Common OAuth Errors
Invalid Client:
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
Invalid Grant:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired or is invalid"
}
Invalid PKCE:
{
"error": "invalid_grant",
"error_description": "PKCE verification failed"
}
Unsupported Grant Type:
{
"error": "unsupported_grant_type",
"error_description": "Grant type not supported"
}
Next Steps
- MCP Server Tools - Complete tools documentation
- Troubleshooting - Common issues and solutions