FedCM 更新:针对 Continuation API 软件包和 Storage Access API 自动授权的源试用

从 Chrome 126 开始,开发者可以开始针对一系列桌面 Federated Credential Management API (FedCM) 功能运行源试用,这些功能支持某些授权用例。该软件包由 Continuation API 和 Parameters API 组成,它们可提供类似于 OAuth 授权流程的体验,涉及身份提供方 (IdP) 提供的权限对话框。该软件包还包含其他更改,例如 fields API、多个 config网址 和自定义帐号标签。从 Chrome 126 开始,我们还将引入 Storage Access API (SAA) 源试用,如果用户过去使用 FedCM 成功登录,它会自动授予 SAA 请求。

源试用:FedCM Continuation API 软件包

FedCM Continuation API 包包含多个 FedCM 扩展程序:

Continuation API

用户正在登录 RP,然后通过按钮模式授权。

您可以查看 Glitch 上的 API 演示

Continuation API 允许 IdP 的 ID 断言端点选择性地返回一个网址,FedCM 将呈现该网址,以便用户继续执行多步登录流程。这样一来,IdP 就可以请求用户授予现有 FedCM 界面中无法实现的信赖方 (RP) 权限,例如对用户服务器端资源的访问权限。

通常,ID 断言端点会返回身份验证所需的令牌。

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

不过,使用 Continuation API 时,ID 断言端点可以返回 continue_on 属性,其中包含 ID 断言端点的绝对路径或相对路径。

{
  // In the id_assertion_endpoint, instead of returning a typical
  // "token" response, the IdP decides that it needs the user to
  // continue on a pop-up window:
  "continue_on": "/oauth/authorize?scope=..."
}

浏览器一收到 continue_on 响应,就会打开一个新的弹出式窗口,并将用户转到指定路径。

当用户与页面互动(例如,授予进一步权限以与 RP 共享额外信息)后,IdP 页面可以调用 IdentityProvider.resolve() 来解析原始 navigator.credentials.get() 调用并以参数形式返回令牌。

document.getElementById('allow_btn').addEventListener('click', async () => {
  let accessToken = await fetch('/generate_access_token.cgi');
  // Closes the window and resolves the promise (that is still hanging
  // in the relying party's renderer) with the value that is passed.
  IdentityProvider.resolve(accessToken);
});

然后,浏览器会自行关闭弹出式窗口,并将令牌返回给 API 调用方。

如果用户拒绝请求,您可以通过调用 IdentityProvider.close() 关闭窗口。

IdentityProvider.close();

如果由于某种原因,用户因某种原因在弹出式窗口中更改了其帐号(例如,IdP 提供了“切换用户”功能,或在委托情况下),则解析调用会接受可选的第二个参数,该参数类似于以下内容:

IdentityProvider.resolve(token, {accountId: '1234');

参数 API

Parameters API 允许 RP 向 ID 断言端点提供其他参数。借助 Parameters API,RP 可以将其他参数传递给 IdP,以请求对基本登录以外的资源的权限。用户将通过由 IdP 控制的用户体验流程(该流程通过 Continuation API 启动)授权这些权限。

如需使用该 API,请将参数作为 navigator.credentials.get() 调用中的对象添加到 params 属性中。

let {token} = await navigator.credentials.get({
  identity: {
    providers: [{
      clientId: '1234',
      configURL: 'https://idp.example/fedcm.json',
      // Key/value pairs that need to be passed from the
      // RP to the IdP but that don't really play any role with
      // the browser.
      params: {
        IDP_SPECIFIC_PARAM: '1',
        foo: 'BAR',
        ETC: 'MOAR',
        scope: 'calendar.readonly photos.write',
      }
    },
  }
});

params 对象中的属性名称以 param_ 作为前缀。在上面的示例中,params 属性包含 IDP_SPECIFIC_PARAM'1')、foo'BAR')、ETC'MOAR')和 scope'calendar.readonly photos.write')。在请求的 HTTP 正文中,这将转换为 param_IDP_SPECIFIC_PARAM=1&param_foo=BAR&param_ETC=MOAR&param_scope=calendar.readonly%20photos.write

POST /fedcm_assertion_endpoint 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=234234&disclosure_text_shown=false&param_IDP_SPECIFIC_PARAM=1&param_foo=BAR&param_ETC=MOAR&param_scope=calendar.readonly%20photos.write

动态获取权限

一般而言,对于用户而言,在需要时(而不是在开发者认为它们最容易实现)时请求权限最为有用。例如,最好在用户即将拍照时请求相机使用权限,而不是在用户进入网站后立即请求权限。相同的做法也适用于服务器资源。仅在用户需要权限时请求权限。这称为“动态授权”。

要通过 FedCM 动态请求授权,IdP 可以执行以下操作:

  1. 使用 IdP 可以理解的必需参数调用 navigator.credentials.get(),例如 scope
  2. ID 断言端点确认用户已登录,并返回 continue_on 网址进行响应。
  3. 浏览器会打开一个弹出式窗口,其中包含 IdP 的权限页面,该页面会请求提供与请求范围匹配的额外权限。
  4. IdP 通过 IdentityProvider.resolve() 授权后,该窗口就会关闭,RP 的原始 navigator.credentials.get() 调用将获得相关令牌或授权代码,以便 RP 可以将它与正确的访问令牌交换。

字段 API

Fields API 允许 RP 声明要向 IdP 请求的帐号属性,以便浏览器在 FedCM 对话框中呈现适当的披露界面;IdP 有责任在返回的令牌中添加所请求的字段。考虑这会在 OpenID Connect 中请求“基本配置文件”与在 OAuth 中请求“范围”。

微件模式下的披露消息。
微件模式下的披露消息。
按钮模式下的披露消息。
按钮模式下的披露消息。

如需使用 Fields API,请在 navigator.credentials.get() 调用中将参数作为数组添加到 fields 属性。这些字段目前可以包含 'name''email''picture',但将来可以进行扩展以包含更多值。

带有 fields 的请求如下所示:

let { token } = await navigator.credentials.get({
  identity: {
    providers: [{
      fields: ['name', 'email', 'picture'],
      clientId: '1234',
      configURL: 'https://idp.example/fedcm.json',
      params: {
        scope: 'drive.readonly calendar.readonly',
      }
    },
  }
  mediation: 'optional',
});

发送到 ID 断言端点的 HTTP 请求包含 RP 指定的 fields 参数(如果该参数不是回访用户,则将 disclosure_text_shown 参数设置为 true),以及浏览器在 disclosure_shown_for 参数中向用户披露的字段:

POST /id_assertion_endpoint 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=234234&disclosure_text_shown=true&fields=email,name,picture&disclosure_shown_for=email,name,picture

如果 RP 需要从 IdP 访问任何其他数据(例如访问日历),则应使用上述自定义参数进行处理。IdP 会返回一个 continue_on 网址以请求权限。

如果 fields 是空数组,请求将如下所示:

let { token } = await navigator.credentials.get({
  identity: {
    providers: [{
      fields: [],
      clientId: '1234',
      configURL: 'https://idp.example/fedcm.json',
      params: {
        scope: 'drive.readonly calendar.readonly',
      }
    },
  }
  mediation: 'optional',
});

如果 fields 是一个空数组,则用户代理会跳过披露界面。

在微件模式下不显示披露消息。在按钮流程中,系统会完全跳过披露界面。
在微件模式下不显示披露声明消息。在按钮流程中,系统会完全跳过披露界面。

即使来自帐号端点的响应不包含与 approved_clients 中的 RP 匹配的客户端 ID,也是如此。

在这种情况下,发送到 ID 断言端点disclosure_text_shown 在 HTTP 正文中为 false:

POST /id_assertion_endpoint 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=234234&disclosure_text_shown=false

多个 config网址

多个 config网址 允许 IdP 容纳 IdP 的多个配置文件,方法是在已知文件中指定与配置文件相同的 accounts_endpointlogin_url

如果将 accounts_endpointlogin_url 添加到已知文件中,则 provider_urls 会被忽略,以便 IdP 可以支持多个配置文件。否则,provider_urls 会继续生效,以使其向后兼容。

支持多个 config网址 的知名文件可能如下所示:

{
  "provider_urls": [ "https://idp.example/fedcm.json" ],
  "accounts_endpoint": "https://idp.example/accounts",
  "login_url": "https://idp.example/login"
}

这使我们能够:

  1. 保持对现有已知文件和已实际部署的早期版本的浏览器的向后和向前兼容性。
  2. 拥有任意数量的配置文件 - 只要它们都指向相同的 accounts_endpointlogin_url
  3. 熵没有机会添加到向 accounts_endpoint 发出的基于凭据的提取请求,因为必须在“.well-known”级别指定。

支持多个 config网址 并非强制性要求,并且现有 FedCM 实现可以保持不变。

自定义账号标签

自定义帐号标签允许 FedCM IdP 为帐号添加注释,以便 RP 可以通过在配置文件中指定标签来过滤帐号。通过在 navigator.credentials.get() 调用中指定 Domain Hint APILogin Hint API,也可以进行类似的过滤,但自定义帐号标签可以通过指定配置文件来过滤用户,这在使用多个 config网址 时特别有用。自定义帐号标签也有所不同,因为自定义帐号标签由 IdP 服务器提供,而不是由 RP(如登录或网域提示)提供。

示例

IdP 分别支持为个人用户和企业用户两个 config网址。消费者配置文件具有 'consumer' 标签,企业配置文件具有 'enterprise' 标签。

通过这样的设置,已知文件会包含 accounts_endpointlogin_url,以允许使用多个 config网址。

{
  "provider_urls": [ "https://idp.example/fedcm.json" ],
  "accounts_endpoint": "https://idp.example/accounts",
  "login_url": "https://idp.example/login"
}

如果在已知文件中提供了 accounts_endpoint,系统会忽略 provider_urls。RP 可以直接指向 navigator.credentials.get() 调用中的相应配置文件。

使用方配置文件位于 https://idp.example/fedcm.json,其中包含使用 include 指定 'consumer'accounts 属性。

{
  "accounts_endpoint": "https://idp.example/accounts",
  "client_metadata_endpoint": "/client_metadata",
  "login_url": "https://idp.example/login",
  "id_assertion_endpoint": "/assertion",
  "accounts": {
    "include": "consumer"
  }
}

企业配置文件位于 https://idp.example/enterprise/fedcm.json,其中包含使用 include 指定 'enterprise'accounts 属性。

{
  "accounts_endpoint": "https://idp.example/accounts",
  "client_metadata_endpoint": "/enterprise/client_metadata",
  "login_url": "https://idp.example/login",
  "id_assertion_endpoint": "/assertion",
  "accounts": {
    "include": "enterprise"
  }
}

通用 IdP 帐号端点(在此示例中为 https://idp.example/accounts)会返回一个帐号列表,其中包含标签属性,并且每个帐号的数组中都分配有 labels。以下是针对拥有两个帐号的用户的响应示例。一个面向消费者,另一个面向企业:

{
 "accounts": [{
   "id": "123",
   "given_name": "John",
   "name": "John Doe",
   "email": "john_doe@idp.example",
   "picture": "https://idp.example/profile/123",
   "labels": ["consumer"]
  }], [{
   "id": "4567",
   "given_name": "Jane",
   "name": "Jane Doe",
   "email": "jane_doe@idp.example",
   "picture": "https://idp.example/profile/4567",
   "labels": ["enterprise"]
  }]
}

当 RP 想要允许 'enterprise' 用户登录时,他们可以在 navigator.credentials.get() 调用中指定 'enterprise' config网址 'https://idp.example/enterprise/fedcm.json'

let { token } = await navigator.credentials.get({
  identity: {
    providers: [{
      clientId: '1234',
      nonce: '234234',
      configURL: 'https://idp.example/enterprise/fedcm.json',
    },
  }
});

因此,用户只能使用 '4567' 的账号 ID 进行登录。'123' 的帐号 ID 会被浏览器以静默方式隐藏,这样就不会向用户提供此网站上 IdP 不支持的帐号。

源试用:FedCM 作为 Storage Access API 的信任信号

Chrome 126 将开始将 FedCM 作为 Storage Access API 的信任信号的源试用。进行此更改后,通过 FedCM 事先授予的权限将成为 Storage Access API 自动批准存储空间访问请求的有效原因。

当嵌入式 iframe 想要访问个性化资源时,这非常有用:例如,如果 idp.example 嵌入在 rp.example 中并且需要显示个性化资源时。如果浏览器限制对第三方 Cookie 的访问权限,那么即使用户通过 FedCM 使用 idp.example 登录 rp.example,嵌入式 idp.example iframe 也无法请求个性化资源,因为请求不包含第三方 Cookie。

为此,idp.example 需要通过网站上嵌入的 iframe 来获取存储空间访问权限,而这只能通过权限提示获取。

使用 FedCM 作为 Storage Access API 的信任信号时,Storage Access API 权限检查不仅接受存储访问提示授予的权限,还接受 FedCM 提示授予的权限。

// In top-level rp.example:

// Ensure FedCM permission has been granted.
const cred = await navigator.credentials.get({
  identity: {
    providers: [{
      configURL: 'https://idp.example/fedcm.json',
      clientId: '123',
    }],
  },
  mediation: 'optional',
});

// In an embedded IdP iframe:

// No user gesture is needed to call this, and the call will be auto-granted.
await document.requestStorageAccess();

// This returns `true`.
const hasAccess = await document.hasStorageAccess();

用户使用 FedCM 登录后,只要 FedCM 身份验证有效,系统就会自动授予权限。这意味着,一旦用户断开连接,请求权限就会显示一条提示。

参与源试用

通过在 Chrome 126 或更高版本上开启 Chrome 标志 chrome://flags#fedcm-authz,您可以在本地试用 FedCM Continuation API 软件包。您还可以通过在 Chrome 126 或更高版本上启用 #fedcm-with-storage-access-api,在本地尝试将 FedCM 作为 Storage Access API 的信任信号。

这些功能也以源试用的形式提供。通过源试用,您可以试用新功能,并就其易用性、实用性和有效性提供反馈。如需了解详情,请参阅开始试用

如需试用 FedCM Continuation API 软件包源试用,请创建两个源试用令牌:

如果您有兴趣启用 Continuation API 以及按钮流程,请同时启用 Button Mode API 源试用

如需尝试 FedCM 作为 Storage Access API 源试用的信任信号,请执行以下操作:

Chrome 126 开始提供 Continuation API 软件包源试用版和作为 Storage Access API 源试用版信任信号的 FedCM。

为 RP 注册第三方源试用

  1. 转到源试用注册页面。
  2. 点击 Register(注册)按钮,然后填写申请令牌的表单。
  3. Web Origin 字段中输入 IdP 的来源。
  4. 检查第三方匹配,以便在其他源上使用 JavaScript 注入令牌。
  5. 点击提交
  6. 将颁发的令牌嵌入第三方网站。

如需在第三方网站上嵌入令牌,请将以下代码添加到 IdP 的 JavaScript 库或从 IdP 的来源提供的 SDK。

const tokenElement = document.createElement('meta');
tokenElement.httpEquiv = 'origin-trial';
tokenElement.content = 'TOKEN_GOES_HERE';
document.head.appendChild(tokenElement);

TOKEN_GOES_HERE 替换为您自己的令牌。