FedCM for IndieAuth

From IndieWeb

FedCM for IndieAuth is a guide intended to help IndieAuth developers add support for the FedCM APIs, including the IdP Registration feature that enables FedCM to be used with IndieAuth.

Note that the language used in FedCM is slightly different from the OAuth/IndieAuth terminology. In particular:

  • IdP - short for "Identity Provider", also called "authorization server" or "IndieAuth server"
  • RP - short for "Relying Party", known in OAuth/IndieAuth as the "client"

See FedCM for a demo video of the resulting flow.

IndieAuth Servers

FedCM Endpoints

This guide assumes you are starting with a functional IndieAuth server, including authorization endpoint and token endpoint.

The first part of this will be setting up the core FedCM endpoints. You can follow the MDN IdP Integration guide for this.

well-known

Create a .well-known/web-identity endpoint at your IndieAuth server's domain. This MUST be at the eTLD+1, and cannot be at a subdomain. For example, if your IndieAuth server is login.example.com this file must go at example.com/.well-known/web-identity. (See idp-registration issue #9 for a possible DNS alternative.)

The well-known file must be served with the application/json content type. The content of the file is a JSON object with the full URL to the IdP's configURL.

https://example.com/.well-known/web-identity

{
  "provider_urls": ["https://login.example.com/fedcm/config.json"]
}

IdP config file

The IdP config file contains links to other FedCM endpoints. This is a JSON document as well.

https://login.example.com/fedcm/config.json

{
  "accounts_endpoint": "/accounts",
  "client_metadata_endpoint": "/client_metadata",
  "id_assertion_endpoint": "/assertion",
  "login_url": "/login"
}

The properties are as follows:

  • accounts_endpoint - The browser will make a request to this endpoint to determine if the user is logged in and fetch their photo/name to display in the widget
  • client_metadata_endpoint - This doesn't really make sense for the IndieAuth version of FedCM, but this normally would be how the IdP returns the client's terms of service and privacy policy URLs to the browser. See idp-registration issue #8 for an alternative that lets the client provide these URLs instead, as well as OAuth Client Metadata Document for the format of the client metadata document used in IndieAuth.
  • id_assertion_endpoint - This is the endpoint the browser gets a credential from after the user clicks log in. For IndieAuth, this will be where the IndieAuth server returns an authorization code to the browser.
  • login_url - If the user is not logged in, the browser will launch a popup window to this URL so the user can log in.

Note that the browser will not follow any redirects, these URLs in the config file must respond with JSON data directly.

accounts endpoint

The accounts endpoint is expected to return the list of accounts the user is logged in to at the IdP. For an IndieAuth server, this is likely a single account.

The browser will make a GET request to this endpoint along with any cookies for this domain, but does not contain a client_id, Origin or Referer header.

GET /accounts HTTP/1.1
Host: login.example.com
Accept: application/json
Cookie: ebc7940893e0c4035e9179d0b83
Sec-Fetch-Dest: webidentity

The IdP checks the Sec-Fetch-Dest header and validates the cookie to determine which user is logged in, then returns the account information:

{
  "accounts": [
    {
      "id": "example.com",
      "given_name": "Example",
      "name": "Example User",
      "email": "example.com",
      "picture": "https://example.com/photo.jpg"
    }
  ]
}

Note: The email value will be displayed in the account chooser in the browser. This does not have to actually be an email address though, so you can return the user's IndieAuth profile URL there instead. See issue 639 for a proposal to use "display name" instead of email.

If the user is not signed in, the IdP should return an HTTP 401 response.

client metadata endpoint

The browser will request the client metadata from the IdP at this endpoint. This doesn't make sense for IndieAuth, since clients control their own metadata and the IdP may not even know about the client ahead of time.

See idp-registration issue #8 for an alternative that lets the client provide these URLs instead. I don't think it makes sense to fully specify what this could look like for IndieAuth, since the issue will likely be resolved by allowing the client to provide the URLs.

In the mean time, you'll need to return something here like the below:

{
  "privacy_policy_url": "https://client.example.com/privacy_policy.html",
  "terms_of_service_url": "https://client.example.com/terms_of_service.html"
}

ID assertion endpoint

This endpoint is what makes it all work. After the user clicks the browser "continue" button, the browser makes a request to this endpoint finally linking the RP and the IdP. The request is application/x-www-form-urlencoded, and includes parameters with details of the attempted sign-in, as well as cookies from the IdP.

  • client_id - The identifier of the client, which in the case of IndieAuth, is the client's URL, e.g. https://webmention.io/
  • account_id - The ID of the account the user selected from the accounts endpoint

Note: in the future, the RP will probably be able to pass arbitrary parameters to this endpoint (custom-requests issue #2). This will allow the RP to use PKCE to better secure the end-to-end flow. In the mean time, the RP can include a PKCE code_challenge in the nonce parameter.

This request will contain cookies, so the IdP will know if the user is logged in and which user is logged in. Other validation steps the IdP does at this stage include:

  • Ensure the Sec-Fetch-Dest: webidentity header is present
  • Ensure the host name of the Origin header matches the client_id host name

At this point, the IdP can return data to the RP. The IdP builds a JSON string containing an IndieAuth authorization code as well as the IndieAuth server metadata URL. The client will later exchange this authorization code for user profile information. The response should look like the below, including the CORS headers:

Content-type: application/json
Access-Control-Allow-Origin: https://client.example.org
Access-Control-Allow-Credentials: true

{
  "token": "{\"code\":\"<authorization code>\",\"metadata_endpoint\":\"<indieauth-metadata-endpoint>\"}"
}

The double JSON encoding is not great, since it also means the RP has to JSON-decode this. See FedCM issue 578 to track progress on allowing the IdP to return JSON instead of a "token" string.

Note: The Access-Control-Allow-Origin header must not contain a trailing slash. If you don't set the right CORS headers here, the browser will throw an error in the console.

IdP Implementation Notes

Sec-Fetch-Dest header

For any requests that include cookies, the browser will send a Sec-Fetch-Dest: webidentity header as well. The IdP MUST validate the presence of this header to protect against CSRF attacks. This applies to the Accounts and ID Assertion endpoints.

Login Status API

The Login Status API allows an IdP to inform the browser of its login status. It is possible for the IdP to set this status either from JavaScript or an HTTP header. The IdP should update the status when the user logs in or logs out.

Setting the login status from an HTTP header:

Set-Login: logged-in

Set-Login: logged-out

Setting the login status from JavaScript:

navigator.login.setStatus("logged-in");

navigator.login.setStatus("logged-out");

If the IdP does not do this, the RP's call to FedCM will fail with the error "Not signed in with the identity provider" before the browser even attempts to fetch the accounts endpoint.

Cookies

The IdP cookies must be set to SameSite=None, HTTPOnly, and Secure. If these properties are not set, the browser will not send the cookies to the accounts or assertion endpoint, so it will always appear that the user is logged out.

The SameSite=None requirement was added in Chrome 125. See FedCM issue 587 to track a proposal to change this to something else.

IdP Registration

The magic that makes this work for IndieAuth is the IdP Registration API. This is an experimental API that is in Chrome behind a feature flag. See idp-registration issue #2 for background and the full history of this feature.

At some point, you need to get the user to click a button to register the IdP with the browser. Provide a button, and when clicked, call the register function, providing the full URL of the configURL:

IdentityProvider.register('https://login.example.com/config.json');

This will prompt the user to register this as an IdP in their browser, making it available to RPs.

Note: User interaction is required in order to call this function, you can't just run this when the page loads.

IndieAuth Clients

As an RP (client) using FedCM for IndieAuth, the process is as follows:

  • Call navigator.credentials.get with a configURL of "any" to ask for any IdP rather than a specific IdP
  • Upon receiving the authorization code (in the "token" property of the returned IdentityCredential), exchange the authorization code for the user's profile info at the user's token endpoint

A detailed version of these steps is below.

navigator.credentials.get

First, from JavaScript, call navigator.credentials.get with configURL: "any". If the user is logged in, this will pop up the widget in the corner asking if they want to continue logging in to this site.

Until the RP can include arbitrary data to the assertion endpoint, you can use the nonce parameter to include a PKCE code_challenge. (This code_challenge should be generated by your server and passed to the JS, ensuring that only your server ever has the code_verifier value.)

    const identityCredential = await navigator.credentials.get({
      identity: {
        context: "signin",
        providers: [
          {
            configURL: "any",
            clientId: window.location.origin+"/", // Use the scheme+host+path as the client ID
            nonce: "<code_challenge>", // this is probably going away https://github.com/fedidcg/FedCM/issues/556
          },
        ]
      },
    }).catch(e => {
      console.log("Error", e.message);
    });

    // If successful, identityCredential.token will be a JSON string with the authorization code and IndieAuth metadata URL

If the user clicks the "Continue" button, the browser will make a request with the IdP cookies to the ID assertion endpoint and return the response to your JavaScript code.

For IndieAuth, the response returned will be an authorization code. You'll need to exchange the authorization code for the user's profile information at the user's token endpoint.

Typically an RP has a server-side component, so this also means your server can retrieve the information directly from the token endpoint rather than accepting the data from the browser where it wouldn't be trustworthy without further validation.

Your JS will get two values back from the API: identityCredential.token and identityCredential.configURL. This is the first time you will know anything about which IdP the user has selected. The token is actually a JSON-encoded string with the authorization code and the IndieAuth server metadata URL.

So at this point, write some JavaScript to send this authorization code and IndieAuth metadata URL up to your server to continue the work.

    if(identityCredential && identityCredential.token) {

      const {code, metadata_endpoint} = JSON.parse(identityCredential.token);

      const response = await fetch("/fedcm-login", {
        method: "POST",
        headers: {
          "Content-type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          code: code,
          metadata_endpoint: metadata_endpoint
        })
      });
      
      const responseData = await response.json();

      // responseData will contain whatever your server responded with
    }

Exchange the authorization code

Your server-side code will receive the two values (code and metadata_endpoint) from your JavaScript. It now needs to do some validation and attempt to exchange the code for the user's profile information, then you can start the session on the server and log the user in to the site.

Fetch the metadata_endpoint URL and look for the token_endpoint property.

Make an OAuth token request to the token endpoint including the authorization code and PKCE code_verifier. There is no redirect_uri since this is not a redirect-based flow.

POST https://login.example.com/token
Content-type: application/x-www-form-urlencoded
Accept: application/json

grant_type=authorization_code
&code=<authorization code>
&client_id=https://app.example.net/
&code_verifier=a6128783714cfda1d388e2e98b6ae8221ac31aca31959e59512c59f5

The response will be the typical IndieAuth response, including the me URL, profile info, and optional access token. https://indieauth.spec.indieweb.org/#profile-information

Last you'll need to verify that this IndieAuth server is authorized to make claims about the user identified by the me URL returned. Since the me URL returned by the server can be any arbitrary URL, not just a URL on the same domain, you need to fetch the URL and look for the matching IndieAuth metadata URL in either the HTTP header or link rel. This way you can be sure that the user has allowed this IndieAuth server to make claims about the user's website.

Once confirmed, your server can log the user in.