FedCM updates: Origin trials for the Continuation API bundle and Storage Access API auto-grant

From Chrome 126, developers can start running an origin trial for a bundle of desktop Federated Credential Management API (FedCM) features that enable some Authorization use cases. The bundle consists of the Continuation API and the Parameters API, which enable an OAuth authorization flow-like experience involving an identity provider (IdP)-provided permission dialog. The bundle also includes other changes such as the Fields API, Multiple configURLs, and Custom Account Labels. From Chrome 126, we are also introducing an origin trial for the Storage Access API (SAA) that auto-grants SAA requests if the user has successfully logged in using FedCM in the past.

Origin Trial: FedCM Continuation API bundle

The FedCM Continuation API bundle consists of multiple FedCM extensions:

Continuation API

A user is signing in to the RP then authorize through the button mode.

You can check out a demo of the API on Glitch.

The Continuation API allows the IdP's ID assertion endpoint to optionally return a URL that FedCM will render to allow the user to continue a multi-step sign-in flow. This allows the IdP to request the user to grant the relying party (RP) permissions beyond what is possible in the existing FedCM UI, such as access to the user's server-side resources.

Typically, the ID assertion endpoint returns a token required for authentication.

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

However, with the Continuation API, the ID assertion endpoint can return a continue_on property which includes an absolute path or a relative path to the ID assertion endpoint.

{
  // 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=..."
}

As soon as the browser receives the continue_on response, a new popup window is opened and navigates the user to the specified path.

After the user interacts with the page, for example granting further permission to share extra information with the RP, the IdP page can call IdentityProvider.resolve() to resolve the original navigator.credentials.get() call and return a token as an argument.

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

The browser will then close the popup by itself and return the token to the API caller.

If the user rejects the request, you can close the window by calling IdentityProvider.close().

IdentityProvider.close();

If for some reason the user has changed their account in the popup (for example the IdP offers a "switch user" function, or in delegation cases), the resolve call takes an optional second argument allowing something like:

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

Parameters API

The Parameters API allows the RP to provide additional parameters to the ID assertion endpoint. With the Parameters API, RPs can pass additional parameters to the IdP to request permissions for resources beyond basic sign-in. The user would authorize these permissions through an IdP-controlled UX flow that is launched via the Continuation API.

To use the API, add parameters to the params property as an object in the navigator.credentials.get() call.

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',
      }
    },
  }
});

The property names in the params object are prepended with param_. In the above example, the params property contains IDP_SPECIFIC_PARAM as '1', foo as 'BAR', ETC as 'MOAR' and scope as 'calendar.readonly photos.write'. This will be translated as param_IDP_SPECIFIC_PARAM=1&param_foo=BAR&param_ETC=MOAR&param_scope=calendar.readonly%20photos.write in the HTTP body of the request:

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

Get permissions dynamically

In general, for users it is most helpful to request permissions when they are needed, rather than when the developer feels they are easiest to implement. For example, asking for permission to access a camera when the user is about to take a photo is preferred to asking for permission as soon as the user lands on the website. The same practice applies to server resources. Request permissions only when they're needed for the user. This is called "dynamic authorization".

To request authorization dynamically with FedCM, the IdP can:

  1. Call navigator.credentials.get() with required parameters the IdP can understand, such as scope.
  2. The ID assertion endpoint confirms the user is already signed in and responds with a continue_on URL.
  3. The browser opens a popup window with the IdP’s permission page asking for additional permission that matches the requested scopes.
  4. Once authorized via IdentityProvider.resolve() by the IdP, the window is closed and the RP's original navigator.credentials.get() call gets a relevant token or an authorization code so that the RP can exchange it with a proper access token.

Fields API

The Fields API allows the RP to declare account attributes to request from the IdP so that the browser can render a proper disclosure UI in the FedCM dialog; it's the IdP's responsibility to include the requested fields in the returned token. Consider this requesting a "basic profile" in OpenID Connect versus "scopes" in OAuth.

Disclosure message in widget mode.
Disclosure message in widget mode.
Disclosure message in button mode.
Disclosure message in button mode.

To use Fields API, add parameters to the fields property as an array in the navigator.credentials.get() call. The fields can contain 'name', 'email' and 'picture' for now, but can be expanded to include more values in the future.

A request with fields would look like this:

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',
});

The HTTP request to the ID assertion endpoint includes the RP-specified fields parameter, with the disclosure_text_shown parameter set as true if this is not a returning user, and the fields that the browser disclosed to the user in a disclosure_shown_for parameter:

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

If the RP needs access to any additional data from the IdP, such as access to a calendar, this should be handled with a custom parameter as mentioned above. The IdP returns a continue_on URL to request permission.

If fields is an empty array, the request would look like this:

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',
});

If fields is an empty array, the user agent will skip the disclosure UI.

Disclosure message is not displayed in the widget mode. In the button flow, the disclosure UI is skipped entirely.
Disclosure message is not displayed in the widget mode. In the button flow, the disclosure UI is skipped entirely.

This is the case even if the response from the accounts endpoint does not contain a client ID that matches the RP in approved_clients.

In this case, the disclosure_text_shown sent to the ID assertion endpoint is false in the HTTP body:

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

Multiple configURLs

Multiple configURLs allow IdPs to accommodate multiple config files for an IdP, by specifying accounts_endpoint and login_url in the well-known file the same as the config files.

If accounts_endpoint and login_url are added to the well-known file, the provider_urls are ignored so that the IdP can support multiple config files. If they aren't, provider_urls continue to take effect so that it is backward compatible.

The well-known file that supports multiple configURLs can look like this:

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

This allows us to:

  1. Maintain backwards and forwards compatibility with existing well-known files and the earlier version of browsers that are already deployed in the wild.
  2. Have an arbitrary number of config files—as long as they all point to the same accounts_endpoint and login_url.
  3. Have no opportunity for entropy to be added to the credentialed fetch request made to the accounts_endpoint, since it has to be specified at the ".well-known" level.

Supporting multiple configURLs is optional and the existing FedCM implementations can stay the same.

Custom Account Labels

Custom Account Labels allow FedCM IdPs to annotate accounts so that RPs can filter them by specifying the label in a config file. Similar filtering has been possible using the Domain Hint API and the Login Hint API by specifying them in the navigator.credentials.get() call, but the Custom Account Labels can filter users by specifying the config file, which is especially useful when multiple configURLs are used. Custom Account Labels are also different in that they are provided from the IdP server, as opposed to from the RP, like login or domain hints.

Example

An IdP supports two configURLs for consumer and enterprise respectively. The consumer config file has an 'consumer' label, and the enterprise config file has a 'enterprise' label.

With such a setup, the well-known file includes accounts_endpoint and login_url to allow multiple configURLs.

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

When the accounts_endpoint is provided in the well-known file, the provider_urls are ignored. The RP can point directly at respective config files in the navigator.credentials.get() call.

The consumer config file is at https://idp.example/fedcm.json which includes accounts property that specifies 'consumer' using the include.

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

The enterprise config file is at https://idp.example/enterprise/fedcm.json, which includes the accounts property that specifies 'enterprise' using the include.

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

The common IdP accounts endpoint (in this example https://idp.example/accounts) returns a list of accounts that includes a labels property with assigned labels in an array for each account. The following is an example response for a user who has two accounts. One is for consumer and the other is for enterprise:

{
 "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"]
  }]
}

When an RP wants to allow 'enterprise' users to sign in, they can specify the 'enterprise' configURL 'https://idp.example/enterprise/fedcm.json' in the navigator.credentials.get() call:

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

As a result, only the account ID of '4567' is available for the user to sign in. The account ID of '123' is silently hidden by the browser so that the user won't be provided with an account that's not supported by the IdP on this site.

Origin trial: FedCM as a trust signal for the Storage Access API

Chrome 126 is starting an origin trial of FedCM as a trust signal for the Storage Access API. With this change, a prior permission grant via FedCM becomes a valid reason to automatically approve a storage access request by the Storage Access APIs.

This is useful when an embedded iframe wants to access personalized resources: for example, if idp.example is embedded in rp.example and needs to show a personalized resource. If the browser restricts access to third-party cookies, even if the user is signed in to rp.example using idp.example with FedCM, the embedded idp.example iframe won't be able to request personalized resources because requests won't include third-party cookies.

To achieve this, idp.example needs to get a storage access permission via its iframe embedded on the website, and this can only be obtained through a permission prompt.

With FedCM as a trust signal for the Storage Access API, Storage Access API permission checks not only accept the permission grant that is given by a storage access prompt, but also the permission grant given by a FedCM prompt.

// 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();

Once the user is signed in with FedCM, the permission is auto-granted as long as the FedCM authentication is active. This means once the user is disconnected, requesting permission will show a prompt.

Participate in the origin trial

You can try the FedCM Continuation API bundle locally by turning on a Chrome flag chrome://flags#fedcm-authz on Chrome 126 or later. You can also try the FedCM as a trust signal for the Storage Access API locally by turning on #fedcm-with-storage-access-api on Chrome 126 or later.

These features are also available as origin trials. Origin trials allow you to try new features and give feedback on their usability, practicality, and effectiveness. For more information, check out the Get started with origin trials.

To try the FedCM Continuation API bundle origin trial, create two origin trial tokens:

If you are interested in enabling the Continuation API along with the button flow, enable the Button Mode API origin trial as well:

To try the FedCM as a trust signal for the Storage Access API origin trial:

The Continuation API bundle origin trial and the FedCM as a trust signal for the Storage Access API origin trial are available from Chrome 126.

Register a third-party origin trial for the RP

  1. Go to the origin trial registration page.
  2. Click the Register button and fill out the form to request a token.
  3. Enter the IdP's origin as Web Origin.
  4. Check Third-party matching to inject the token with JavaScript on other origins.
  5. Click Submit.
  6. Embed the issued token on a third-party website.

To embed the token on a third-party website, add the following code to the IdP's JavaScript library or SDK served from the IdP's origin.

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

Replace TOKEN_GOES_HERE with your own token.