跳转到主要内容

概览

大多数健康数据源以 OAuth 2.0 授权。在 Mirobody 中,这在每个 C++ vendor::Vendor 内经三个契约操作处理 —— authorize_urlhandle_webhookrevoke —— 另外,对于浏览器驱动的 SMART-on-FHIR EHR,还有一个独立的有状态连接服务。
代码库中存在两种 OAuth 角色,别把它们混淆:
  • src/oauth/ —— Mirobody 作为一个 OAuth 2.0 授权服务器(它向 API 客户端签发 token;OIDC 发现 + PKCE)。
  • Vendor 客户端 —— Mirobody 作为某个健康源(Fitbit、某个 EHR 等) OAuth 客户端。本页讲的是客户端这一侧。

OAuth 2.0 —— authorize_url 步骤

authorize_url 构建你重定向用户去的同意 URL。它是纯字符串构建 —— 无网络调用。Fitbit(device/fitbit.cpp)是参考:保密型服务端客户端在 token 步骤用其 secret 认证,因此这里不使用 PKCE
std::string authorize_url(const std::string& redirect_uri, const std::string& state,
                          const std::string& /*user_id*/, const std::string& /*provider*/) override {
    if (config().client_id.empty()) {
        throw VendorError(info_.id + ": authorize_url requires an OAuth client_id — set "
                          "MIROBODY_VENDOR_FITBIT_CLIENT_ID");
    }
    std::string url = "https://www.fitbit.com/oauth2/authorize"
                      "?response_type=code&client_id=" + url_encode(config().client_id) +
                      "&scope=" + url_encode("activity heartrate sleep weight profile");
    if (!redirect_uri.empty()) url += "&redirect_uri=" + url_encode(redirect_uri);
    if (!state.empty())        url += "&state=" + url_encode(state);
    return url;
}
  • state 是你的关联 / CSRF 值,会在回调时原样回传。
  • token 交换(在该 vendor 的 /oauth2/tokencode 换 access token)在带外完成;所得的 access token 就是你存为 MIROBODY_VENDOR_<ID>_API_KEY 并在 fetch 时传入的那个。

撤销

revoke 撤销授权。Fitbit 把 token POST 到 /oauth2/revoke,以保密客户端身份认证(HTTP Basic client_id:client_secret):
req.url     = base_url() + "/oauth2/revoke";
req.body    = "token=" + url_encode(config().api_key);
req.headers = { "Authorization: Basic " +
                storage::base64_encode(config().client_id + ":" + config().client_secret) };
Garmin 的 revoke 则调用其有文档的账号断连 hook(DELETE /wellness-api/rest/user/registration,Bearer)。Withings 没有公开的 token 撤销端点,所以它的 revoke 是一个诚实的桩。

验证 webhook

当某个源对其 webhook 签名时,在信任请求体之前先验证它。Fitbit 用 X-Fitbit-Signature = base64(HMAC-SHA1(raw_body))(以 client_secret + "&" 为密钥)签名;客户端重新计算并以恒定时间比较:
const std::string key = config().client_secret + "&";
std::array<unsigned char, 20> mac = storage::hmac_sha1(key, body);
if (!ct_equal(storage::base64_encode(mac.data(), mac.size()), sig)) {
    throw VendorError(info_.id + ": webhook signature verification failed (X-Fitbit-Signature)");
}
若签名方案不公开(如 Garmin 的未签名推送),handle_webhook 保留为桩 —— Mirobody 不会捏造一个验证方案。

PKCE 与有状态流程(SMART on FHIR、Garmin)

某些流程需要 PKCE:在 authorize 时铸造的 code_verifier 必须被持久化,并在 token 交换时重放。裸 authorize_url 无处存放该 verifier,因此 PKCE 同意住在一个有状态的连接层里,而不在 vendor 的 authorize_url 中。这正是 Garmin 的 authorize_url 是桩的原因。 已发布的示例是 SMART-on-FHIR EHR 连接服务src/health/ehr_connect.*),其中 Mirobody 是某医院 EHR 的 OAuth 客户端:
1

Authorize

POST /health/ehr/authorize {fhir_base_url} 运行 SMART 发现{base}/.well-known/smart-configuration),生成 PKCE verifier + state缓存它们,并返回供浏览器使用的该租户 authorize_url
2

Callback

GET /health/ehr/callback?code=&state= —— 无 bearer;用户由一次性的 state 恢复。缓存的 verifier 被重放,用于在该租户的 token 端点交换 code,access token 按用户缓存。
3

Sync

POST /health/ehr/sync 使用缓存的 token,经 ehr 客户端获取 Observation 并持久化它们。
SMART_FHIR_* 系列键配置(client id / redirect uri / scope)。PKCE 生成本身有单元测试(tests/oauth/pkce_test.cpp)。

安全须知

access token 存于 VendorConfig.api_key,源自 MIROBODY_VENDOR_<ID>_*。名称中含 _KEY / _SECRET / _TOKEN 的配置值会在加载时自动加密(Fernet)。绝不记录 token。
authorize_url 传入一个随机 state,并在回调时验证它。EHR 连接服务使用一个一次性的、同时携带用户绑定的缓存 state
用恒定时间比较(ct_equal)比对 webhook 签名,绝不用 ==
无公开签名方案 → webhook 为桩。无有文档的撤销 → revoke 为桩。诚实胜过捏造。

下一步

OAuth 2.0 示例(Fitbit)

一个完整的 OAuth 2.0 设备客户端

Garmin 示例

为何 PKCE / 推送保留为桩

Provider 集成

构建一个新的 vendor::Vendor

使用 Provider

端到端的 EHR 连接流程