OpenEMR API client_credentials grant_type

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