Skip to main content

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

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": "Example AI Integration",
"redirect_uris": ["https://example.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://example.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_access scope enables refresh tokens
  • Save the client_id for subsequent requests

Step 3: Generate PKCE Parameters

PKCE (Proof Key for Code Exchange) prevents authorization code interception.

Generate Code Verifier:

// Generate 32 bytes of random data (produces 43-character base64url string)
const codeVerifier = base64URLEncode(crypto.randomBytes(32))
// 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://example.ai/oauth/callback&
scope=patient/*.read offline_access&
state=random_state_string_xyz&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256

Parameters:

ParameterRequiredDescription
response_typeYesMust be code
client_idYesFrom registration
redirect_uriYesMust match registered URI
scopeYesSpace-separated scopes
stateYesRandom string for CSRF protection
code_challengeYesSHA256 hash of code_verifier
code_challenge_methodYesMust be S256

Response:

The server redirects to the HealthEx consent page:

https://app.healthex.io/#/patient-consent/{projectId}/enrollment/link?oauth_state=base64_params

The patient:

  1. Logs in or creates a HealthEx account
  2. Reviews what data the AI agent is requesting
  3. Grants or denies consent
  4. Identifies which health care organizations they want to fetch data from
  5. Logs in to each health care organization that requires it

The system will then begin performing the following tasks in the background:

  • Retrieves patient records from connected EHRs
  • Processes records for search

The authorization code is issued only after all 5 of the above steps are completed by the patient.

Note: The above steps may take a significant amount of time, depending on the patient's speed, the number of medical records they have, and whether their health care organizations require further authentication. A patient can commonly take more than 10 minutes to go through the flow. This differs from most other OAuth flows, which often take a minute or less. Ensure that your OAuth client is prepared to handle this. We recommend supporting at least 30 minute OAuth sessions, and ideally unlimited session duration.


Step 6: Authorization Code Issued

After consent and data processing, the patient is redirected back to your application:

Redirect:

https://example.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://example.ai/oauth/callback&
client_id=550e8400-e29b-41d4-a716-446655440000&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Parameters:

ParameterRequiredDescription
grant_typeYesMust be authorization_code
codeYesAuthorization code from redirect
redirect_uriYesMust match authorization request
client_idYesYour client ID
code_verifierYesOriginal 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:

  1. Authorization code is valid and not expired
  2. redirect_uri matches the stored value
  3. client_id matches the code owner
  4. 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"
}

Record Processing

If this is the patient's first time connecting your AI agent, the patient's records may not be ready for use immediately upon return to your application. All MCP endpoints will handle this condition gracefully, and instruct the user to try again shortly.


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 refresh 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