OpenEMR API client_credentials grant_type

OpenEMR API client_credentials grant_type
OpenEMR Logo

As a Python developer working with OpenEMR, I recently encountered the frustrating challenge of implementing the client_credentials grant type. The documentation is surprisingly sparse and misleading, leaving crucial implementation details to guesswork.

The official docs suggest a straightforward OAuth2 implementation, but reality proved otherwise. After hours of debugging and database exploration, I discovered that OpenEMR has a critical bug in its scope handling for client_credentials flows.

The core issue? OpenEMR stores client scopes in the database, but the API doesn't properly recognize them for client_credential grants. The solution required direct database manipulation.

update openemr.oauth_clients oc 
set scope='<all the scopes you need separated by space>'
where client_name = '<your client name>';

Once the scopes are correctly assigned, you can use the following python test script to verify correct retrieval of the access_token and access to the data:

import jwt
import time
import uuid
import requests
import json
import sys
# Your client details
CLIENT_ID = "<your client_id>"
CLIENT_SECRET = "<your client_secret>"

API_URL = "<your api_url>"
TOKEN_URL = f"{API_URL}/oauth2/default/token"
# The audience should match what the server expects
# Load your private key
with open('private_key.pem', 'rb') as f:
    private_key = f.read()
KEY_ID = json.load(open("jwks.json"))['keys'][0]['kid']

# Create JWT payload with longer expiration
now = int(time.time())
payload = {
    "iss": CLIENT_ID,
    "sub": CLIENT_ID,
    "aud": TOKEN_URL,  # yes this is token_url and not AUD url provided when creating a client
    "jti": str(uuid.uuid4()),
    "exp": int(now + 3600),  # 1 hour from now
    "iat": int(now)
}

# Print the payload for debugging
print(f"JWT Payload: {json.dumps(payload, indent=2)}")

# Sign the JWT
assertion = jwt.encode(
    payload,
    private_key,
    algorithm="RS384",
    headers={"kid": KEY_ID, "typ": "JWT"}
)

# Print part of the assertion for debugging
print(f"JWT (first 30 chars): {assertion[:30]}...")

print("Decoded jwt:")
print(jwt.decode(assertion, open('public_key.pem', 'rb').read(), algorithms=["RS384"], audience=TOKEN_URL))

# add other scopes as necessary - they are case sensitive :) 
scope = " ".join([
    "openid",
    "offline_access",
    "api:oemr",
    "user/patient.read",
    "user/patient.write",
    "user/vital.read",
    "user/vital.write",
    "user/medication.read",
    "user/medication.write",
    "user/medical_problem.read",
    "user/medical_problem.write",
    "user/encounter.read",
    "user/encounter.write",
    "fhirUser",
    "online_access",
    "user/Patient.read",
    "system/Patient.read"
])

# Prepare the request
data = {
    'grant_type': 'client_credentials',
    'client_id': CLIENT_ID,
    'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    'client_assertion': assertion,
    'scope': scope,
}

# Print request data for debugging
print(f"Request data: {json.dumps({k: v[:30]+'...' if k == 'client_assertion' else v for k, v in data.items()}, indent=2)}")

# Make the request with verbose error handling
try:
    response = requests.post(TOKEN_URL, data=data)
    print(f"Status Code: {response.status_code}")
    print(f"Response: {response.text}")
    token_data = response.json()
except Exception as e:
    print(f"Request failed: {str(e)}")
    sys.exit(1)

# get the list of patients
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
url = f"{API_URL}/apis/default/fhir/Patient"
r = requests.get(url, headers=headers)
print(f"Status Code: {r.status_code}")
print(f"Response: {r.text}")

Make sure to use the following info to create your JWKS configuration: https://community.open-emr.org/t/use-fhir-in-open-emr-v7/19117/2