FedCM for IndieAuth
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 widgetclient_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 theclient_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 aconfigURL
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.
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.