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验证私钥有效性,确保无密码保护或加载时正确传递密码。
- 尝试将签名算法改为RS256,同步修改JWK中的
- 简化测试范围:将
scope改为最小权限(比如仅system/Patient.read),排除权限范围导致的验证失败。 - 抓包验证请求完整性:用抓包工具确认请求的所有参数、头部都正确发送,无篡改;将JWT在本地解码验证,确保签名、声明都符合要求。
内容的提问来源于stack exchange,提问作者SScotti
相关产品推荐
相关产品推荐

