Situation
I’m trying to create an app that can add new patients and update their records through the OpenEMR Standard API. I’ve spun up a test instance of OpenEMR through the official docker image, configured it for API access, registered an app and enabled it. When I try to request an authorization token, I get an error (see “Logs” below for error, and “Appendix” below for all the code involved).
OpenEMR Version
7.0.2
Browser:
n/a
Operating System
OpenEMR is running in the official Docker image, the host is running MacOS 14.5. Also reproduced on an Ubuntu 24.04.1 host.
Search
Did you search the forum for similar questions? Yes
Logs
The error message:
{"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"}
When I look inside the container in /var/log/apache2/error.log, I see:
[Thu Sep 05 02:40:41.070239 2024] [php:notice] [pid 198] [client 10.107.99.26:53636] [2024-09-05T02:40:41.066100+00:00] OpenEMR.ERROR: CustomClientCredentialsGrant->validateClient() jwt failed required constraints {"client":"19w7eEMUVA1wFfgvMgXel6r1SzbObXAdXqpSHmVV9fI","exceptionMessage":"The token violates some mandatory constraints, details:\\n- Token signer mismatch","claims":{"iss":"19w7eEMUVA1wFfgvMgXel6r1SzbObXAdXqpSHmVV9fI","sub":"19w7eEMUVA1wFfgvMgXel6r1SzbObXAdXqpSHmVV9fI","aud":["http://localhost/oauth2/default/token"],"jti":"deadbeef"},"expectedAudience":"http://localhost/oauth2/default/token"} []
Apendix
This code is in Python, with dependencies requests
, jwt
, and jwcrypto
.
The Python code I use to generate the JWKS:
from pathlib import Path
from jwcrypto import jwk
key_type = "RSA"
alg = "RS256"
size = 2048
use = "sig"
key_name = "test"
key = jwk.JWK.generate(kty=key_type, size=size, kid=key_name, use=use, alg=alg)
keys_path = Path("keys")
keys_path.mkdir()
private_path = keys_path / f"{key_name}_private.json"
public_path = keys_path / f"{key_name}_public.json"
pem_path = keys_path / f"{key_name}.pem"
private_path.write_text(key.export_private())
public_path.write_text(key.export_public())
pem_path.write_text(key.export_to_pem("private_key", password=None).decode("utf-8"))
The code I use to register the API client:
import sys
from pathlib import Path
import json
import requests
REG_URL = "http://localhost/oauth2/default/registration"
app_name = "test"
jwks_path = Path("keys/test_public.json")
jwks_json = {"keys": [json.loads(jwks_path.read_text())]}
payload = {
"client_name": app_name,
"contacts": [""],
"application_type":"private",
"redirect_uris": [""],
"initiate_login_uri": "",
"post_logout_redirect_uris": [""],
"token_endpoint_auth_method": "client_secret_post",
"scope": "openid fhirUser online_access offline_access launch launch/patient api:oemr api:fhir api:port system/Patient.$export system/Group.$export system/*.$bulkdata-status system/*.$export profile name address given_name family_name nickname phone phone_verified email email_verified site:default system/AllergyIntolerance.read system/Appointment.read system/Binary.read system/CarePlan.read system/CareTeam.read system/Condition.read system/Coverage.read system/Device.read system/DiagnosticReport.read system/DocumentReference.$docref system/DocumentReference.read system/Encounter.read system/Goal.read system/Group.read system/Immunization.read system/Location.read system/Medication.read system/MedicationRequest.read system/Observation.read system/Organization.read system/Patient.read system/Person.read system/Practitioner.read system/PractitionerRole.read system/Procedure.read system/Provenance.read system/ValueSet.read",
"jwks_uri": "",
"jwks": jwks_json,
}
r = requests.post(REG_URL, json=payload)
response_json = json.loads(r.text)
out_path = Path("registration.json")
out_path.write_text(json.dumps(response_json, indent=2))
Finally, the code to make the request:
import json
import os
from pathlib import Path
import requests
from jwt import JWT, jwk_from_dict
import urllib3
key_path = Path("keys")
public_key_file = key_path / "test_public.json"
public_key_dict = json.loads(public_key_file.read_text())
private_key_file = key_path / "test_private.json"
private_key_dict = json.loads(private_key_file.read_text())
api_file = Path("registration.json")
api_dict = json.loads(api_file.read_text())
TOKEN_URL = "http://localhost/oauth2/default/token"
header = {
"alg": public_key_dict["alg"],
"typ": "JWT",
"kid": public_key_dict["kid"],
}
print(header)
payload = {
"iss": api_dict["client_id"],
"sub": api_dict["client_id"],
"aud": TOKEN_URL,
"jti": "deadbeef",
}
print(payload)
signing_key = jwk_from_dict(private_key_dict)
instance = JWT()
compact_jws = instance.encode(payload, signing_key, alg=public_key_dict["alg"])
print(compact_jws)
token_request = {
"scope": api_dict["scope"],
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": compact_jws,
}
r = requests.post(TOKEN_URL, data=token_request, verify=False)
print(r.text)
Any insights are much appreciated. Thanks!