You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

eClinicalWorks沙箱FHIR认证失败:JWT遇401 invalid_client错误求助

eClinicalWorks沙箱JWT认证失败(401 invalid_client)问题

背景

已成功实现AthenaHealth的Basic Authentication认证、EPIC沙箱的JWT认证,但对接eClinicalWorks沙箱时,相同JWT代码无法完成认证,返回401 invalid_client。已与eClinicalWorks技术支持确认配置无误,且手动验证JWT有效。

相关操作与代码

1. AthenaHealth Basic Auth认证代码

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']

2. 生成公私钥对的OpenSSL命令

# 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

3. 生成JWK集合的Python代码

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

4. eClinicalWorks沙箱JWT认证测试代码

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

错误信息

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

解决建议

  • 核对JWKS与JWT的kid一致性:确保JWKS文件中的kid和JWT头部的kid完全匹配,包括大小写、哈希格式。同时确认JWKS_URL能被eClinicalWorks的OAuth服务器公开访问,无访问限制。
  • 调整JWT声明细节:
    • 替换aud字段为AUTH_URL测试,部分厂商要求受众为授权端点而非令牌端点;
    • 检查本地机器时间与eClinicalWorks服务器时间的偏差,确保iat和exp在服务器允许的时间窗口内(建议偏差不超过60秒);
    • 确认iss和sub严格等于Staging环境的CLIENT_ID,无额外字符。
  • 补充请求参数:在请求的form data中添加client_id字段,值为你的Staging CLIENT_ID,部分厂商的client_credentials模式需要显式传递该参数。
  • 验证密钥与算法:
    • 尝试将签名算法改为RS256,同步修改JWK中的alg字段并更新JWKS;
    • 重新生成证书,将CN字段设为你的CLIENT_ID(subj '/CN=你的CLIENT_ID'),再重新生成JWKS;
    • 用openssl rsa -in private_key.pem -check验证私钥有效性,确保无密码保护或加载时正确传递密码。
  • 简化测试范围:将scope改为最小权限(比如仅system/Patient.read),排除权限范围导致的验证失败。
  • 抓包验证请求完整性:用抓包工具确认请求的所有参数、头部都正确发送,无篡改;将JWT在本地解码验证,确保签名、声明都符合要求。

内容的提问来源于stack exchange,提问作者SScotti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:23:16