"Token signer mismatch" when requesting auth token using JWKS

Taking the queue from @TimM and trying to fix client_credentials grant_type, I was able to get client_credentials to work. The keys were

  1. Tim’s RS384 tip
  2. Updating scopes directly in the database

Make sure to reference JWKS info found at Use FHIR in open EMR V7 - #2 by Mandrake

Update scopes with direct db access using

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

And test with the following python script:

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}")

I created a written article for the fix at OpenEMR API client_credentials grant_type to hopefully make it easier for future me to stumble upon.