身份提供方与 FedCM 集成

本文详细介绍了身份提供方 (IdP) 与联邦凭证管理 (FedCM) API 集成所需的所有步骤。

IdP 集成步骤

要与 FedCM 集成,IdP 需要执行以下操作:

  1. 提供一个 well-known 文件来识别 IdP。
  2. 提供一个配置文件和端点,用于帐户列表和断言颁发(以及可选的客户端元数据)。
  3. 使用登录状态 API 更新其登录状态

提供一个 well-known 文件

存在一个潜在的隐私问题,即 IdP 能够在未经明确同意的情况下识别用户是否访问了依赖方 (RP)。这具有跟踪含义,因此 IdP 需要提供一个 well-known 文件来验证其身份并缓解此问题。

well-known 文件是通过非凭证 GET 请求的,该请求不遵循重定向。这有效地阻止了 IdP 了解谁发出了请求以及哪个 RP 正在尝试连接。

well-known 文件必须从 IdP 的 eTLD+1 处在 /.well-known/web-identity 提供。例如,如果 IdP 端点在 https://accounts.idp.example/ 下提供,则它们必须在 https://idp.example/.well-known/web-identity 处提供一个 well-known 文件。well-known 文件的内容应具有以下 JSON 结构:

json
{
  "provider_urls": ["https://accounts.idp.example/config.json"]
}

provider_urls 成员应包含一个 URL 数组,指向有效的 IdP 配置文件,RP 可以使用这些文件与 IdP 交互。数组长度目前限制为一。

Sec-Fetch-Dest HTTP 标头

所有从浏览器通过 FedCM 发送的请求都包含 Sec-Fetch-Dest: webidentity 标头。所有接收凭证请求的 IdP 端点(即 accounts_endpointid_assertion_endpoint)都必须确认包含此标头,以防范 CSRF 攻击。

提供一个配置文件和端点

IdP 配置文件提供了浏览器处理身份联邦流程和管理登录所需的端点列表。这些端点需要与配置文件同源。

浏览器通过 GET 方法对配置文件发出非凭证请求,该请求不遵循重定向。这有效地阻止了 IdP 了解谁发出了请求以及哪个 RP 正在尝试连接。

配置文件(在我们的示例中托管在 https://accounts.idp.example/config.json)应具有以下 JSON 结构:

json
{
  "accounts_endpoint": "/accounts.php",
  "account_label": "developer",
  "supports_use_other_account": true,
  "client_metadata_endpoint": "/client_metadata.php",
  "disconnect_endpoint": "/disconnect.php",
  "id_assertion_endpoint": "/assertion.php",
  "login_url": "/login",
  "branding": {
    "background_color": "green",
    "color": "0xFFEEAA",
    "icons": [
      {
        "url": "https://idp.example/icon.ico",
        "size": 25
      }
    ]
  }
}

属性如下:

accounts_endpoint

帐户列表端点的 URL,该端点返回用户当前在 IdP 上登录的帐户列表。浏览器使用这些帐户在浏览器提供的 FedCM UI 中创建登录选项列表以显示给用户。

account_label 可选

如果包含,则指定用于联合身份验证时应返回的帐户子集的标识符。当发出 get() 请求时,只有在 帐户端点label_hints 参数匹配此字符串的帐户才会被返回。

supports_use_other_account 可选

一个布尔值,默认为 false;如果设置为 true,则表示用户可以使用与当前登录帐户不同的帐户登录(如果 IdP 支持多个帐户)。这仅适用于指定了 活动模式get() 调用。

注意:在浏览器登录 UI 中,这很可能表现为某种“选择其他帐户”按钮。

client_metadata_endpoint 可选

客户端元数据端点的 URL,该端点提供指向 RP 元数据和使用条款页面的 URL,用于 FedCM UI 中。

disconnect_endpoint 可选

断开连接端点的 URL,RP 通过 IdentityCredential.disconnect() 方法使用该端点与 IdP 断开连接。

id_assertion_endpoint

ID 断言端点的 URL,当发送有效用户凭证时,该端点应返回一个验证令牌,RP 可以使用该令牌验证身份验证。

login_url

用户登录 IdP 的登录页面 URL。

branding 可选

包含品牌信息,该信息将在浏览器提供的 FedCM UI 中使用,以根据 IdP 的需要自定义其外观。在被动模式下,提供的图标大小必须大于或等于 25 (25px),在活动模式下必须大于或等于 40 (40px)(有关更多详细信息,请参阅活动模式与被动模式)。

下表总结了 FedCM API 发出的不同请求:

端点/资源 方法 凭证式(带 cookie) 包含 Origin
well-known/config.json GET
accounts_endpoint GET
client_metadata_endpoint GET
disconnect_endpoint POST
id_assertion_endpoint POST

注意:有关 FedCM 流程中这些端点访问的描述,请参阅FedCM 登录流程

注意:出于隐私目的,FedCM API 对此处详细介绍的端点发出的所有请求都不允许遵循重定向。

帐户列表端点

浏览器通过 GET 方法向此端点发送凭证请求(即,带有标识已登录用户的 cookie)。请求没有 client_id 参数、Origin 标头或 Referer 标头。这有效地阻止了 IdP 了解用户正在尝试登录哪个 RP。返回的帐户列表是与 RP 无关的。

例如

http
GET /accounts.php HTTP/1.1
Host: idp.example
Accept: application/json
Cookie: 0x23223
Sec-Fetch-Dest: webidentity

成功请求的响应返回用户当前已登录的所有 IdP 帐户列表(不特定于任何特定的 RP),其 JSON 结构如下:

json
{
  "accounts": [
    {
      "id": "elaina_maduro",
      "given_name": "Elaina",
      "name": "Elaina Maduro",
      "email": "elaina_maduro@idp.example",
      "tel": "+491234567890",
      "username": "elaina420",
      "picture": "https://idp.example/profile/123",
      "approved_clients": ["123", "456", "789"],
      "domain_hints": ["rp1.example.com", "rp3.example.com"],
      "label_hints": ["developer", "admin"],
      "login_hints": ["elaina_maduro", "elaina_maduro@idp.example"]
    },
    {
      "id": "elly",
      "given_name": "Elly",
      "username": "elly123",
      "email": "Elly@idp.example",
      "picture": "https://idp.example/profile/456",
      "approved_clients": ["abc", "def", "ghi"],
      "domain_hints": ["rp1.example.com", "rp2.example.com"],
      "label_hints": ["developer", "test"],
      "login_hints": ["elly", "elly@idp.example"]
    }
  ]
}

这包括以下信息,其中 nameemailusernametel 是可选的,但其中至少一个必须存在且非空。

id

用户的唯一 ID。

name 可选

用户的姓氏。

email 可选

用户的电子邮件地址。

tel 可选

用户的电话号码。可以是任何格式。

username 可选

用户的用户名。

given_name 可选

用户的名字。

picture 可选

用户头像图片的 URL。

approved_clients 可选

用户已注册的 RP 客户端数组。

domain_hints 可选

与帐户关联的域数组。RP 可以发出包含 domainHint 属性的 get() 调用,以按域筛选返回的帐户。

label_hints 可选

指定标识帐户类型的标签字符串数组。如果配置文件指定了 account_label,则只有在其 label_hints 中包含该标签的帐户才会从帐户列表端点返回。

login_hints 可选

表示帐户的字符串数组。这些字符串用于筛选浏览器为用户登录提供的帐户选项列表。当在相关的 get() 调用中 identity.providers 中提供了 loginHint 属性时,会发生这种情况。任何在其 login_hints 数组中包含与提供的 loginHint 匹配的字符串的帐户都将包含在内。

注意:如果用户未登录任何 IdP 帐户,则端点应返回 HTTP 401 (Unauthorized)

客户端元数据端点

浏览器通过 GET 方法向此端点发送非凭证请求,并将 clientId 作为参数传递到 get() 调用中。

例如

http
GET /client_metadata.php?client_id=1234 HTTP/1.1
Host: idp.example
Origin: https://rp.example/
Accept: application/json
Sec-Fetch-Dest: webidentity

成功请求的响应包含指向 RP 元数据和使用条款页面的 URL,用于浏览器提供的 FedCM UI。这应遵循以下 JSON 结构:

json
{
  "privacy_policy_url": "https://rp.example/privacy_policy.html",
  "terms_of_service_url": "https://rp.example/terms_of_service.html"
}

断开连接端点

通过调用 IdentityCredential.disconnect(),浏览器向断开连接端点发送一个跨域 POST 请求,其中包含 cookie 和 application/x-www-form-urlencodedContent-Type,以及以下信息:

account_hint

一个字符串,指定 IdP 用于识别要断开连接的帐户的帐户提示。

client_id

一个字符串,指定 RP 的客户端标识符。

例如

http
POST /disconnect HTTP/1.1
Host: idp.example
Origin: rp.example
Content-Type: application/x-www-form-urlencoded
Cookie: 0x123
Sec-Fetch-Dest: webidentity

account_hint=account456&client_id=rp123

收到请求后,IdP 服务器应:

  1. 使用 CORS(跨域资源共享)响应请求。

  2. 验证请求是否包含带有 webidentity 指令的 Sec-Fetch-Dest HTTP 标头。

  3. Origin 标头与由 client_id 确定的 RP 源匹配。如果不匹配,则拒绝 promise。

  4. 查找与 account_hint 匹配的帐户。

  5. 将用户帐户从 RP 连接帐户列表中断开连接。

  6. 以 JSON 格式响应已识别用户的 account_id

    json
    {
      "account_id": "account456"
    }
    

注意:如果 IdP 希望断开与 RP 关联的所有帐户,它可以传递一个不匹配任何 account_id 的字符串,例如 "account_id": "*"

ID 断言端点

浏览器通过 POST 方法向此端点发送凭证请求,内容类型为 application/x-www-form-urlencoded。请求还包含一个负载,其中包括有关尝试登录和要验证的帐户的详细信息。

它应该看起来像这样:

http
POST /assertion.php HTTP/1.1
Host: idp.example
Origin: https://rp.example/
Content-Type: application/x-www-form-urlencoded
Cookie: 0x23223
Sec-Fetch-Dest: webidentity
account_id=123&client_id=client1234&nonce=Ct60bD&disclosure_text_shown=true&is_auto_selected=true

当用户从相关的浏览器 UI 中选择一个帐户进行登录时,会向此端点发送请求。当发送有效用户凭证时,此端点应返回一个验证令牌,RP 可以根据他们用于身份联邦的 IdP 概述的使用说明,在自己的服务器上使用该令牌验证身份验证。一旦 RP 验证了用户,他们就可以登录用户、将他们注册到其服务等等。

json
{
  "token": "***********"
}

请求负载包含以下参数:

client_id

RP 的客户端标识符(与原始 get() 请求中的 clientId 匹配)。

account_id

要登录的用户帐户的唯一 ID(与帐户列表端点响应中用户的 id 匹配)。

nonce 可选

由 RP 提供的请求随机数。

disclosure_text_shown

一个字符串,值为 "true""false",指示是否显示了披露文本。披露文本是向用户显示的信息(如果提供了,可以包括服务条款和隐私政策链接),如果用户已登录到 IdP 但在当前 RP 上没有特定帐户(在这种情况下,他们需要选择“继续作为...”其 IdP 身份,然后在 RP 上创建相应的帐户)。

is_auto_selected

一个字符串,值为 "true""false",指示身份验证验证请求是否由于自动重新身份验证而发出,即没有用户干预。当 get() 调用以 "optional""silent"mediation 选项值发出时,可能会发生这种情况。对于 IdP 来说,了解是否发生了自动重新身份验证对于性能评估和在需要更高安全性时很有用。例如,IdP 可以返回一个错误代码,告诉 RP 它需要明确的用户干预(mediation="required")。

注意:如果 get() 调用成功,则 is_auto_selected 值也会通过 IdentityCredential.isAutoSelected 属性传达给 RP。

ID 断言端点的 CORS 标头

ID 断言端点响应必须包含 Access-Control-Allow-OriginAccess-Control-Allow-Credentials 标头,并且 Access-Control-Allow-Origin 必须包含请求者的源:

http
Access-Control-Allow-Origin: https://rp.example
Access-Control-Allow-Credentials: true

请注意,Access-Control-Allow-Origin 必须设置为请求者(RP)的特定源,并且不能是通配符值 *

如果没有这些标头,请求将因网络错误而失败。

ID 断言错误响应

如果 IdP 无法颁发令牌(例如,如果客户端未经授权),ID 断言端点将返回一个错误响应,其中包含有关错误性质的信息。例如:

json
{
  "error": {
    "code": "access_denied",
    "url": "https://idp.example/error?type=access_denied"
  }
}

错误响应字段如下:

code 可选

一个字符串。这可以是 OAuth 2.0 指定错误列表 中的已知错误,也可以是任意字符串。

url 可选

一个 URL。这应该是一个网页,其中包含有关错误的人类可读信息,以便显示给用户,例如如何修复错误或联系客户服务。URL 必须与 IdP 的配置 URL 同站点。

此信息可以通过几种不同的方式使用:

  • 浏览器可以向用户显示自定义 UI,告知他们出了什么问题(请参阅Chrome 文档以获取示例)。请记住,如果请求因 IdP 服务器不可用而失败,它显然无法返回任何信息。在这种情况下,浏览器将通过通用消息报告此情况。
  • 用于尝试登录的相关 RP navigator.credentials.get() 调用将拒绝其 promise,并带有一个包含错误信息的 IdentityCredentialError。RP 可以捕获此错误,然后通过一些信息来帮助用户在未来的登录尝试中成功,从而跟进浏览器的自定义 UI。

使用登录状态 API 更新登录状态

登录状态 API 允许 IdP 通知浏览器它在该特定浏览器中的登录(登录)状态 — 通过此,我们指的是“是否有任何用户在该浏览器中登录到 IdP”。浏览器为每个 IdP 存储此状态;FedCM API 随后使用它来减少向 IdP 发出的请求数量(因为它无需在没有用户登录到 IdP 时浪费时间请求帐户)。它还缓解了潜在的时序攻击

对于每个已知的 IdP(由其配置 URL 标识),浏览器都维护一个表示登录状态的三态变量,具有三个可能的值:

  • "logged-in":IdP 至少有一个用户帐户已登录。请注意,在此阶段,RP 和浏览器不知道是哪个用户。有关特定用户的信息将在 FedCM 流程的后期从 IdP 的 accounts_endpoint 返回。
  • "logged-out":所有 IdP 帐户当前都已注销。
  • "unknown":此 IdP 的登录状态未知。这是默认值。

设置登录状态

当用户登录或注销 IdP 时,IdP 应更新其登录状态。这可以通过两种不同的方式完成:

  • Set-Login HTTP 响应标头可以在顶级导航或同源子资源请求中设置:

    http
    Set-Login: logged-in
    
    Set-Login: logged-out
    
  • Navigator.login.setStatus() 方法可以从 IdP 源调用:

    js
    /* Set logged-in status */
    navigator.login.setStatus("logged-in");
    
    /* Set logged-out status */
    navigator.login.setStatus("logged-out");
    

登录状态如何影响联合登录流程

RP 尝试联合登录时,会检查登录状态:

  • 如果 IdP 的登录状态为 "logged-in",则会向帐户列表端点发出请求,并在浏览器提供的 FedCM 对话框中向用户显示可用于登录的帐户。
  • 如果所有 IdP 的登录状态都为 "logged-out",则 FedCM get() 请求返回的 promise 将被拒绝,而不会向帐户列表端点发出请求。在这种情况下,由开发人员处理流程,例如提示用户去登录合适的 IdP。
  • 如果 IdP 的登录状态为 "unknown",则会向帐户列表端点发出请求,并且登录状态会根据响应进行更新:
    • 如果端点返回可用于登录的帐户列表,则将状态更新为 "logged-in",并在浏览器提供的 FedCM 对话框中向用户显示登录选项。
    • 如果端点不返回任何帐户,则将状态更新为 "logged-out";如果其他 logged-in IdP 不可用,FedCM get() 请求返回的 promise 将被拒绝。

如果浏览器和 IdP 登录状态不同步怎么办?

尽管登录状态 API 通知浏览器 IdP 登录状态,但浏览器和 IdP 有可能不同步。例如,IdP 会话可能会过期,这意味着所有用户帐户都已注销,但登录状态仍然设置为 "logged-in"(应用程序无法将登录状态设置为 "logged-out")。在这种情况下,当尝试联合登录时,将向 IdP 的帐户列表端点发出请求,但由于会话不再可用,将不会返回任何可用帐户。

发生这种情况时,浏览器可以通过在对话框中打开 IdP 的登录页面来动态地让用户登录 IdP(登录 URL 在 IdP 的配置文件 login_url 中找到)。此流程的具体性质取决于浏览器;例如,Chrome 以这种方式处理它

用户登录 IdP 后,IdP 应:

另见