SScotti
SScotti

Reputation: 2338

Authentication/API Token retrieval for eClinicalWorks, other FHIR endpoints

QUESTION WAS EDITED TO INCLUDE SOME DEVELOPMENT CODE.

Just curious about various methods to authenticate with FHIR through various EMR vendors. I have sandbox accounts with AthenaHealth, eClinicalWorks and EPIC. I've had some success with Athenahealth using BasicAuthentication with a clientID/Secret to retrieve a token, and then the token can be used to make subsequent requests with proper authorization for the various scopes. I have also gotten the EPIC FHIR sandbox to work with JWTs.

My current problem is that I have thus far been unable to authenticate with the eClinicalWorks sandbox environment. Similar code actually does work for the EPIC sandbox using the JWT method.

The Python function below works for AtheneHealth using the Basic Auth method where TOKEN_URL='..../oauth2/v1/token'

def get_access_token():
    response = requests.post(
        TOKEN_URL,
        auth=HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET),
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        data={
        'grant_type': 'client_credentials',
        'scope': '....'
        }
    )
    response.raise_for_status()
    return response.json()['access_token']

The ECW documentation is located here: ECW API Documenation

I generated my own public-private key pair using:

# Generate a private key
openssl genrsa -out private_key.pem 2048
# Extract the public key
openssl req -new -x509 -key private_key.pem -out public.pem -subj '/CN=SandboxTester'
# Get the fingerprint
openssl x509 -noout -fingerprint -sha1 -inform pem -in public.pem
# e.g. sha1 Fingerprint=A9:18:FF:9E:A2:99:0F:24:C6:1A:CF:4C:B8:09:0C:0D:0E:5D:49:B3

and then I generate a JWK set using this Python code:

import hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from base64 import urlsafe_b64encode
import json

# Load your public key from the certificate
with open("public.pem", "rb") as cert_file:
    public_key = x509.load_pem_x509_certificate(cert_file.read(), default_backend()).public_key()

# Extract the modulus (n) and exponent (e) from the public key
numbers = public_key.public_numbers()
n = numbers.n
e = numbers.e

# Convert the modulus and exponent to URL-safe base64 encoding
n_b64 = urlsafe_b64encode(n.to_bytes((n.bit_length() + 7) // 8, byteorder='big')).decode('utf-8').rstrip("=")
e_b64 = urlsafe_b64encode(e.to_bytes((e.bit_length() + 7) // 8, byteorder='big')).decode('utf-8').rstrip("=")

# Generate the kid using a SHA-256 fingerprint
pub_key_bytes = public_key.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)
kid = hashlib.sha256(pub_key_bytes).hexdigest()

# Create the JWK structure
jwk = {
    "kty": "RSA",
    "use": "sig",
    "alg": "RS384",
    "n": n_b64,
    "e": e_b64,
    "kid": kid,  # example Key ID, ensure this matches your requirements
    "key_ops": ["verify"],  # example key operations, adjust as needed
    "ext": True
}

# Create the JWK Set structure
jwk_set = {"keys": [jwk]}

# Print the JWK in a pretty JSON format
print(json.dumps(jwk_set, indent=4))

# Save the JWK to a file
with open("jwk.json", "w") as jwk_file:
    json.dump(jwk_set, jwk_file, indent=4)

print("JWK saved to jwk.json")

and then my test script in Python is like this, minus some config values.

import os
import json
import time
import jwt
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import uuid

CLIENT_ID = "omitted"  #Staging
# TOKEN_URL = 'https://oauthserver.eclinicalworks.com/oauth/oauth2/token'    #Production
TOKEN_URL = "https://staging-oauthserver.ecwcloud.com/oauth/oauth2/token"  #Staging
# AUTH_URL = 'https://oauthserver.eclinicalworks.com/oauth/oauth2/authorize' #Production
AUTH_URL = "https://staging-oauthserver.ecwcloud.com/oauth/oauth2/authorize" #Staging

JWKS_URL = "omitted"

# Path to the private key file
private_key_file = "private_key.pem"

# Read the private key from the file
with open(private_key_file, "rb") as key_file:
    private_key = serialization.load_pem_private_key(
        key_file.read(),
        password=None,
        backend=default_backend()
    )


# Generate a signed JWT with RS384
def generate_jwt():
    now = int(time.time())
    exp = now + 300  # Expiration time no more than five minutes in the future
    claims = {
        "iss": CLIENT_ID,
        "sub": CLIENT_ID,
        "aud": TOKEN_URL,
        "exp": exp,
        "iat": now,
        "jti": str(uuid.uuid4())  # Generate a unique JWT ID
    }
    headers = {
        "alg": "RS384",
        "kid": "omitted",
        "typ": "JWT",
        "jku": JWKS_URL  # Optional, if your JWK Set URL is available
    }
    token = jwt.encode(
        payload=claims,
        key=private_key,
        algorithm="RS384",
        headers=headers
    )
    print("Generated JWT:", token)  # Debugging line to check the JWT
    return token


# Function to get a new access token using JWT
def get_access_token():
    signed_jwt = generate_jwt()
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    data = {
        'grant_type': 'client_credentials',
        'scope': 'system/Patient.read system/Encounter.read system/Group.read',  # Adjust scope as needed
        'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        'client_assertion': signed_jwt
    }

    print("Request Headers:", headers)  # Debugging line to check headers
    print("Request Data:", data)  # Debugging line to check data

    response = requests.post(TOKEN_URL, headers=headers, data=data)
    print("Response Status Code:", response.status_code)
    print("Response Body:", response.text)
    response.raise_for_status()  # Raise an HTTPError for bad responses
    return response.json()['access_token']

# Attempt to get the access token
try:
    access_token = get_access_token()
    print("Access Token:", access_token)
except requests.exceptions.HTTPError as err:
    print(f"HTTP error occurred: {err}")
    print(f"Response content: {err.response.content}")
except Exception as err:
    print(f"Other error occurred: {err}")

I have been through it a couple of times with their tech support and they also verified that everything seemed to be setup correctly, and the JWT verifies at least manually.

Any other suggestions appreciated.

The error that I get is:

Response Status Code: 401
Response Body: {"error":"invalid_client"}
HTTP error occurred: 401 Client Error:  for url: https://staging-oauthserver.ecwcloud.com/oauth/oauth2/token
Response content: b'{"error":"invalid_client"}'

Upvotes: 2

Views: 986

Answers (0)

Related Questions