"Token signer mismatch" when requesting auth token using JWKS

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!

Change your algorithm to RS384 and it should be better. Looking at everything else I think it looks fine althrough I’m not a python expert, I can’t see anything else that jumps out at me.

Looks like HL7 changed their links so OpenEMR client credentials documentation doesn’t go to the right page anymore. Here is the client credentials grant documentation we built on if you need another resource. SMART Backend Services: Authorization Guide : /authorization/

1 Like

Switching to RS384 got me one step further! Now I’m getting 401 Unauthorized when I make a request for all patients with the following code:

import json
from pathlib import Path
import requests

access_file = Path("access.json")
access_json = json.loads(access_file.read_text())
header = {"Authorization": f"Bearer {access_json['access_token']}"}

API_URL = "http://localhost/apis/default/api/patient"
r = requests.get(API_URL, headers=header, verify=False)
print(r)

I’m wondering if maybe I’m authorizing the wrong scopes for this request. I got my scopes by using the app registration page on the web interface, setting it to “system client application”, checking everything, and then looking at the request made in the browser console. But this github issue suggests that that may only cover FHIR scopes, whereas I’m looking for api:oemr scopes? I tried again with the following trimmed down list of scopes:

        "scope": "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",

and see in the error.log:

OpenEMR.ERROR: OpenEMR Error: api failed because user role does not have access to the resource {"resource":"/api/patient","userRole":"system"}

If I want to be able to read and create the things in the listed scopes across the system, what scopes should I be requesting?

Thanks!

Figured it out by searching and finding this thread: Api failed because user role does not have access to the resource - #5 by adunsulag

Switched to password grant, and I’m in business.