Identity and Access for AI Systems
Executive Summary
Identity and access management for AI systems is more complex than for traditional applications because AI services operate in three distinct authorization contexts: as a client consuming LLM APIs (API key authentication), as a service exposing AI capabilities to internal applications (OAuth2 / mTLS), and as a data accessor reading PHI from EHRs and data warehouses (SMART on FHIR / enterprise SSO). Each context requires a different authentication mechanism, and the failure to implement any one of them correctly results in either unauthorized data access or service unavailability. This chapter covers the identity and access patterns required for enterprise clinical AI deployments.
Learning Objectives
- Configure service-to-service authentication for AI platform components using mTLS and API keys
- Implement SMART on FHIR authorization for AI-powered clinical applications accessing EHR data
- Design OAuth2 client credential flows for AI services that access data warehouses and internal APIs
- Apply least-privilege access control to AI service accounts and identify the failure modes of over-broad authorization
Business Problem
A Reference Healthcare Organization's AI platform team must navigate four authorization domains simultaneously: the LLM provider (Anthropic, Azure OpenAI) requires API key management with rotation policies; the EHR (Epic FHIR R4) requires SMART on FHIR authorization with patient-scoped access tokens; the clinical data warehouse requires service account authorization with row-level security policies; and internal AI platform consumers (clinical applications, CDS Hooks) require mTLS or API key authentication to the AI gateway.
Without a coherent identity architecture that spans these domains, teams implement ad-hoc credential management — hardcoded API keys, shared service accounts, persistent credentials without rotation — creating security vulnerabilities that are difficult to audit and impossible to remediate quickly in an incident.
Architecture
SMART on FHIR Authorization
import httpx
import base64
import hashlib
import secrets
import urllib.parse
from dataclasses import dataclass
from typing import Optional
from datetime import datetime, timedelta
# Educational example — not for clinical use
@dataclass
class SMARTTokenResponse:
access_token: str
token_type: str
expires_in: int
scope: str
patient: Optional[str] # Set for EHR launch with patient context
encounter: Optional[str]
issued_at: datetime = None
def __post_init__(self):
if self.issued_at is None:
self.issued_at = datetime.utcnow()
@property
def is_expired(self) -> bool:
return datetime.utcnow() >= self.issued_at + timedelta(seconds=self.expires_in - 60)
class SMARTAuthClient:
"""
SMART on FHIR authorization client for clinical AI applications.
Implements backend service authorization (client credentials flow)
for AI services that access FHIR without user context.
Educational example — not for clinical use.
"""
# Minimum necessary FHIR scopes for clinical decision support
# Never request broader scopes than required for the specific use case
CDS_MINIMUM_SCOPES = [
"system/Patient.read",
"system/Encounter.read",
"system/Condition.read",
"system/MedicationRequest.read",
"system/Observation.read",
"system/AllergyIntolerance.read",
]
AI_DOCUMENTATION_SCOPES = CDS_MINIMUM_SCOPES + [
"system/DocumentReference.write", # Additional scope for writing AI documents
]
def __init__(
self,
client_id: str,
private_key_path: str, # RSA private key for JWT assertion
token_endpoint: str,
scopes: list[str],
):
self.client_id = client_id
self.private_key_path = private_key_path
self.token_endpoint = token_endpoint
self.scopes = scopes
self._token_cache: Optional[SMARTTokenResponse] = None
async def get_access_token(self) -> str:
"""
Get a valid access token, using cached token if not expired.
"""
if self._token_cache and not self._token_cache.is_expired:
return self._token_cache.access_token
token = await self._request_new_token()
self._token_cache = token
return token.access_token
async def _request_new_token(self) -> SMARTTokenResponse:
"""
Request a new access token using JWT assertion (client_credentials flow).
Backend service authentication uses a JWT signed with the client's
RSA private key. The FHIR authorization server verifies the JWT
against the registered public key (JWKS endpoint).
Educational example — not for clinical use.
"""
import jwt as pyjwt
with open(self.private_key_path, "rb") as f:
private_key = f.read()
# Build JWT assertion per SMART Backend Services spec
now = datetime.utcnow()
jwt_assertion = pyjwt.encode(
{
"iss": self.client_id,
"sub": self.client_id,
"aud": self.token_endpoint,
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=5)).timestamp()),
"jti": secrets.token_urlsafe(16),
},
private_key,
algorithm="RS384",
)
async with httpx.AsyncClient() as client:
response = await client.post(
self.token_endpoint,
data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": jwt_assertion,
"scope": " ".join(self.scopes),
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
response.raise_for_status()
data = response.json()
return SMARTTokenResponse(
access_token=data["access_token"],
token_type=data["token_type"],
expires_in=data["expires_in"],
scope=data.get("scope", ""),
patient=data.get("patient"),
encounter=data.get("encounter"),
)OAuth2 Client Credentials for Internal Service Authentication
from typing import Optional
import httpx
# Educational example — not for clinical use
class OAuth2ServiceClient:
"""
OAuth2 client credentials flow for AI service-to-service authentication.
Used by AI platform services authenticating to internal APIs:
- AI Gateway authenticating to internal FHIR proxy
- RAG service authenticating to document store API
- Async workers authenticating to notification service
Educational example — not for clinical use.
"""
def __init__(
self,
token_endpoint: str,
client_id: str,
client_secret_name: str, # Secret name in Secrets Manager (never the secret itself)
scopes: list[str],
secrets_client,
):
self.token_endpoint = token_endpoint
self.client_id = client_id
self.client_secret_name = client_secret_name
self.scopes = scopes
self.secrets_client = secrets_client
self._token_cache: Optional[dict] = None
async def get_bearer_token(self) -> str:
"""Get a valid bearer token for service-to-service calls."""
if self._token_cache and self._is_valid(self._token_cache):
return self._token_cache["access_token"]
client_secret = await self.secrets_client.get_secret(self.client_secret_name)
async with httpx.AsyncClient() as client:
response = await client.post(
self.token_endpoint,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": client_secret,
"scope": " ".join(self.scopes),
},
)
response.raise_for_status()
token_data = response.json()
token_data["_fetched_at"] = datetime.utcnow().timestamp()
self._token_cache = token_data
return token_data["access_token"]
def _is_valid(self, token_data: dict) -> bool:
fetched_at = token_data.get("_fetched_at", 0)
expires_in = token_data.get("expires_in", 3600)
age = datetime.utcnow().timestamp() - fetched_at
return age < (expires_in - 60)API Key Management for LLM Providers
import boto3
from functools import lru_cache
from typing import Optional
# Educational example — not for clinical use
class LLMAPIKeyManager:
"""
Centralized API key management for LLM provider credentials.
API keys are stored in AWS Secrets Manager (or Azure Key Vault),
never in application configuration or environment variables in code.
Keys are rotated on a schedule and fetched at runtime.
Educational example — not for clinical use.
"""
LLM_PROVIDER_SECRETS = {
"anthropic": "ai-platform/llm-providers/anthropic-api-key",
"azure_openai": "ai-platform/llm-providers/azure-openai-key",
"openai": "ai-platform/llm-providers/openai-api-key",
}
def __init__(self, region: str = "us-east-1"):
self.secrets_client = boto3.client("secretsmanager", region_name=region)
self._cache: dict[str, tuple[str, float]] = {}
self._cache_ttl = 300 # 5 minutes (secrets rotation interval may be longer)
def get_api_key(self, provider: str) -> str:
"""
Retrieve LLM provider API key from Secrets Manager.
Cached for 5 minutes to reduce Secrets Manager API calls.
"""
secret_name = self.LLM_PROVIDER_SECRETS.get(provider)
if not secret_name:
raise ValueError(f"Unknown LLM provider: {provider}")
import time
cached_key, cached_at = self._cache.get(provider, (None, 0))
if cached_key and (time.time() - cached_at) < self._cache_ttl:
return cached_key
response = self.secrets_client.get_secret_value(SecretId=secret_name)
api_key = response["SecretString"]
self._cache[provider] = (api_key, time.time())
return api_keyEnterprise Considerations
Credential rotation policy: LLM API keys must be rotated on a schedule. For healthcare deployments, a 90-day rotation policy is a common baseline. AWS Secrets Manager and Azure Key Vault support automatic rotation triggers. The AI platform must handle rotation gracefully: fetching the new key from Secrets Manager on next use, without requiring a service restart.
SMART scope minimization: FHIR access scopes must be the minimum necessary for the specific AI use case. A CDS Hook service that reads patient context does not need system/DocumentReference.write. Quarterly scope audits should review all SMART client registrations and remove excess scopes.
Service account proliferation: Each AI service component (RAG service, CDS Hooks service, async workers, AI gateway) should have its own service account with scopes limited to that component's needs. Shared service accounts make it impossible to attribute access events to a specific component and impossible to revoke a single component's access without affecting others.
Audit logging of AI access: All FHIR access by AI services must be logged in the HIPAA audit log. The audit log entry must include: which AI service accessed the data (service account), which patient resource was accessed, the timestamp, and the clinical use case (CDS Hook, RAG query). This is required for HIPAA breach investigation.
Common Mistakes
1. Hardcoding API keys in application configuration. API keys in config files, environment variables, or code are committed to version control, appear in container images, and cannot be rotated without redeployment. Always retrieve credentials at runtime from a secrets management service.
2. Over-broad FHIR scopes. system/*.read grants the AI service read access to every FHIR resource type for every patient — a significant over-privilege. Specify individual resource types and verify minimum necessity with the privacy officer.
3. No token refresh for SMART tokens. SMART access tokens expire (typically in 1 hour for backend services). An AI service that obtains a token at startup and does not implement refresh will fail after the token expires. Always implement token cache with expiry tracking and automatic refresh.
4. Not logging AI-specific FHIR access in the HIPAA audit log. EHR access logs from Epic or Cerner track user-initiated access. AI service access (which is server-to-server) may not be automatically included in the clinical audit log. The AI platform must emit its own audit log entries for every FHIR access.
Best Practices
- Store all credentials (LLM API keys, service account secrets) in a secrets management service; never in code or config files
- Implement 90-day credential rotation for all LLM API keys
- Use SMART on FHIR JWT assertion (client_credentials + JWT) for EHR access; avoid shared username/password
- Request minimum-necessary FHIR scopes; audit quarterly and remove excess
- Assign dedicated service accounts to each AI platform component (never shared)
- Emit AI-specific FHIR access audit log entries for HIPAA compliance
Key Takeaways
- AI systems operate in multiple authorization domains simultaneously; each requires a different credential pattern
- SMART on FHIR client_credentials + JWT assertion is the standard for AI backend service EHR access
- FHIR scopes must be minimum-necessary;
system/*.readis never appropriate for a production AI service - All credentials must be stored in secrets management services and rotated on a defined schedule
- PHI access by AI services must be logged in the HIPAA audit trail with the service account identity and use case
Further Reading
- EHR Integration Patterns — FHIR R4 integration that requires SMART authorization
- Security Considerations — Broader security framework
- HIPAA and AI — HIPAA audit logging requirements
- Networking and API Gateway — API key enforcement at the gateway layer