IdP Registration API

Draft Community Group Report,

This version:
https://w3c-fedid.github.io/idp-registration/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google Inc.)

Abstract

This specification extends the Federated Credential Management API Federated Credential Management API so that the user agent can remember the user’s handles (e.g. @alice.example, alice@social.example or https://alice.example), and so that relying parties can request any identity provider that issues one of them and supports a federation they accept, instead of enumerating identity providers. A handle is registered either by the identity provider, with the user’s permission, or by the user typing it.

Status of this document

This specification was published by the Federated Identity Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

Today, websites that support federated sign-in pick a small number of large IDPs to show in their sign-in flows, because each one takes space and needs a separate integration. Users whose identity lives elsewhere (on a smaller provider, a custom domain, or a server they run themselves) are left out. By contrast, email verification lets any mail server take part without the website allow-listing it.

This specification lets the user agent act as an intermediary that removes the need for allow-listing:

  1. After the user signs in, an IDP registers the user’s handle by calling register(), with the user’s permission.

  2. An RP requests any registered IDP that supports a federation it accepts, by passing a federation instead of a configURL:

    const credential = await navigator.credentials.get({
      identity: {
        providers: [{
          federation: "https://www.w3.org/TR/indieauth/",
          params: { nonce: "..." }
        }, {
          configURL: "https://idp.example/fedcm.json",
          clientId: "123"
        }]
      }
    });
    
  3. A user whose handle isn’t registered yet can type it, for example @alice.example, alice@social.example or https://alice.example. The user agent resolves it to its issuer and registers it (type your handle).

Both ways of registering a handle run the same resolution and add the same handle to the handle registry.

After Alice signs in on https://idp.example, the IDP sets its login status and registers her handle:
await navigator.login.setStatus("logged-in");
await IdentityProvider.register("@alice.idp.example");

The user agent resolves @alice.idp.example to idp.example’s config file, which lists the federations it supports:

{
  "accounts_endpoint": "/accounts",
  "id_assertion_endpoint": "/assertion",
  "login_url": "/login",
  "federations": ["https://www.w3.org/TR/indieauth/", "https://atproto.com"]
}

And its accounts_endpoint includes her handle on her account:

{
  "accounts": [{
    "id": "1234",
    "name": "Alice",
    "handle": "@alice.idp.example",
    "picture": "https://idp.example/alice.jpg"
  }]
}

This specification is written as a set of monkey patches to Federated Credential Management API, shown like this:

A change to an algorithm or definition in another specification.

2. Federations

A federation, such as IndieAuth or atproto, is identified by an https: URL: the URL that RPs pass as federation and that IDPs list in federations. The federation operator is the party that controls the origin of that URL. It is responsible for:

The user agent doesn’t need to understand the federation’s protocol. It only reads the operator metadata.

2.1. Operator metadata

A federation operator serves its operator metadata at the path /.well-known/web-identity-federation on the origin of its federation’s URL. For the federation https://atproto.com, this is https://atproto.com/.well-known/web-identity-federation.

dictionary IdentityFederationOperatorMetadata {
  IdentityProviderBranding branding;
};
branding, of type IdentityProviderBranding

The federation’s branding, in the same format as an IDP’s branding: its name, icons, background_color, and color.

The operator metadata at https://atproto.com/.well-known/web-identity-federation:
{
  "branding": {
    "name": "Atmosphere",
    "icons": [{"url": "https://atproto.com/icon.png", "size": 64}],
    "background_color": "#1185fe",
    "color": "#ffffff"
  }
}
To fetch the operator metadata given a string federation and a globalObject, run the following steps. This returns an IdentityFederationOperatorMetadata or failure.
  1. Let federationUrl be the result of running the URL parser on federation. If federationUrl is failure, or its scheme is not "https", return failure.

  2. Let url be a new URL with scheme "https", host federationUrl’s host, port federationUrl’s port, and path « ".well-known", "web-identity-federation" ».

  3. Run the steps of fetch the config file that fetch the config file and convert it to an IdentityProviderAPIConfig, with url in place of the config URL, globalObject, and IdentityFederationOperatorMetadata in place of IdentityProviderAPIConfig. Return the result.

Note: Like the config file, the operator metadata is fetched without credentials, without a referrer, and from an opaque origin, so the federation operator doesn’t learn who is asking or which RP they are visiting. User agents MAY cache it according to HTTP caching rules.

Because the operator metadata lives at a well-known path on the federation URL’s origin, one origin can operate only one federation. Should the federation URL point at the operator metadata directly instead?

2.2. Branding

To get the federation branding given a federation federation and a globalObject, run the following steps. This returns an IdentityProviderBranding or null.
  1. Let metadata be the result of fetch the operator metadata with federation and globalObject.

  2. If metadata is failure, or metadata["branding"] doesn’t exist, return null.

  3. Return metadata["branding"].

A federation’s federation branding is the branding in its operator metadata, which the user agent obtains with get the federation branding. The user agent uses it to show the user which federation an RP is asking for (§ 6.3 Account selection), the same way Federated Credential Management API uses an IDP’s branding: it uses the name as the federation’s name, picks an icon from icons, and MAY use background_color and color, subject to the same contrast requirements.

Note: This addresses issue 29.

3. Handles

This specification depends on the Infra Standard. Infra Standard

A handle is a globally-unique and human-readable string that identifies the user, that is produced by parse a handle, and that is resolvable by Resolution. A handle isn’t necessarily permanent: a user can change it, or move it to another IDP.

An issuer is an IDP that is the authority for a handle. The user agent identifies an issuer by its site, where it serves its well-known file. Each handle has exactly one issuer at a time, which Resolution finds.

The site of a host host is host’s registrable domain. A host host is a site host if host is a domain, its registrable domain is non-null, and host equals its registrable domain.

The site of alice.social.example is social.example, as long as social.example isn’t a public suffix. If hosting.example is a public suffix, the site of alice.hosting.example is alice.hosting.example.

This section defines how the user agent parses a handle, resolves it to its issuer, and stores the handles the user uses. None of this requires the user agent to understand any identity protocol. It only needs a small set of handle notations (§ 3.1 Parsing) and a single, protocol-agnostic resolution procedure (§ 3.2 Resolution).

3.1. Parsing

Parse a handle accepts the following notations:

Notation Examples Used by
@ + host @alice.example, @alice.social.example atproto
local + @ + host, optionally with a leading @ alice@social.example, @alice@social.example ActivityPub, Nostr (NIP-05)
https: URL https://alice.example/ IndieAuth, Solid, OpenID 2.0
To parse a handle given a string input, run the following steps. This returns a handle or failure.
  1. Let s be input with leading and trailing ASCII whitespace stripped.

  2. If s starts with "https://":

    1. Let url be the result of running the URL parser on s.

    2. If url is failure, return failure.

    3. Let host be url’s host.

    4. Let handle be the result of running the URL serializer on url.

  3. Otherwise:

    1. If s starts with "@", remove the first code point from s.

    2. If s contains "@":

      1. Let local be the part of s before the last "@", and hostString the part after it.

      2. If local is the empty string, return failure.

    3. Otherwise, let local be null and hostString be s.

    4. Let host be the result of running the host parser on hostString.

    5. If host is failure, return failure.

    6. If local is null, let handle be "@" followed by the serialization of host. Otherwise, let handle be local, followed by "@", followed by the serialization of host.

  4. If host is not a domain, return failure.

  5. If host’s registrable domain is null, return failure.

  6. Return handle.

The site of a handle handle is the result of the following steps:
  1. Let host be the host of the result of running the URL parser on handle if handle starts with "https://", and the result of running the host parser on the part of handle after the last "@" otherwise.

  2. Return host’s registrable domain.

Note: Because a handle is always in the canonical form that parse a handle produces, its site is always a site host.

Input Handle Its site
@alice.example, alice.example @alice.example alice.example
@alice.social.example @alice.social.example social.example
alice@social.example, @alice@social.example alice@social.example social.example
https://alice.example https://alice.example/ alice.example
@192.0.2.1, @com failure
Two strings a and b match as handles if the following steps return true:
  1. If a and b both start with "https://":

    1. Let urlA and urlB be the results of running the URL parser on a and b.

    2. If either is failure, return false.

    3. Return whether urlA equals urlB.

  2. Let |a'| and |b'| be the ASCII lowercased a and b.

  3. If |a'| starts with "@", remove its first code point. Do the same for |b'|.

  4. Return whether |a'| is |b'|.

Note: This is the only comparison used on handles. It is applied to an account’s handle, and never to its username or email, so that this mechanism stays separate from how an IDP displays accounts and from email verification.

URL handles need more normalization (fragments such as #me, trailing slashes).

3.2. Resolution

Resolution turns a handle into its issuer’s metadata. It has two steps: resolve the issuer from the handle’s delegation record, and then fetch the issuer’s metadata.

To resolve a handle given a handle handle and a globalObject, run the following steps. This returns issuer metadata or failure.
  1. Let issuer be the result of resolve the issuer of handle. If issuer is failure, return failure.

  2. Return the result of fetch the issuer metadata with issuer and globalObject.

Resolution takes a handle, not arbitrary input. Callers that start from user or script input first run parse a handle, which also normalizes it (for example, Alice.Example becomes @alice.example), and then resolve the result. Resolution stops at the issuer metadata: it doesn’t fetch the config file or check its federations, because each caller needs a different check.

3.2.1. Resolve the issuer

The first step of resolution finds a handle’s issuer by looking up a delegation record on the handle’s site. Its iss= value names the issuer’s site, whether the site hosts its own IDP or delegates to another site.

To resolve the issuer of a handle handle, run the following steps. This returns the site host of handle’s issuer, or failure.
  1. Let records be the result of look up the delegation records for handle’s site.

  2. If records is failure or its size is not 1, return failure.

  3. Let value be records[0] with the leading "iss=" removed.

  4. Let issuer be the result of running the host parser on the ASCII lowercased value.

  5. If issuer is failure or is not a site host, return failure.

  6. Return issuer.

Note: When a provider’s domain is a public suffix (for example, hosting.example, where each user gets their own subdomain), each subdomain is its own site. The site of @alice.hosting.example is alice.hosting.example, not hosting.example.

Handle Its site Delegation record Issuer
@alice.example alice.example iss=alice.example alice.example
@alice.example alice.example iss=social.example social.example
@alice.social.example social.example iss=social.example social.example
alice@social.example social.example iss=social.example social.example
https://alice.example/ alice.example iss=alice.example alice.example
@alice.hosting.example (hosting.example is a public suffix) alice.hosting.example iss=alice.hosting.example alice.hosting.example
@alice.example alice.example none failure
@alice.example alice.example iss=pds.social.example failure (not a site)

3.2.2. DNS delegation record

A handle’s site names its issuer by publishing a DNS TXT record Domain names - implementation and specification at the name _web-identity. followed by the site:

_web-identity.alice.example.   TXT   "iss=social.example"

This record says that the issuer of handles whose site is alice.example is on the site social.example. If alice.example is its own issuer, the record is:

_web-identity.alice.example.   TXT   "iss=alice.example"

The record’s value MUST be the string "iss=" followed by a site host. There MUST be exactly one such record at a name.

Note: This is modeled on the DNS delegation in the Email Verification Protocol. Unlike that record, the value here names a site rather than a host, so that resolution always ends at a site’s well-known file.

Should the record name be _web-identity or something more specific (e.g. _web-handle)? Should it carry a federation, so that one domain can delegate different protocols to different IDPs?

To look up the delegation records for a site host site, the user agent performs a DNS query for TXT records at the name _web-identity. followed by site, using its DNS resolver. This returns a list of strings (the records whose contents start with "iss="), or failure if the query failed.

3.2.3. Issuer metadata

Once the issuer is known, the user agent fetches its issuer metadata: the well-known file that Federated Credential Management API already defines, an IdentityProviderWellKnown, served at https:// followed by the issuer’s site and /.well-known/web-identity. For the issuer social.example, this is https://social.example/.well-known/web-identity.

Each issuer metadata has an associated issuer, the site host it was fetched from.

The issuer metadata at https://social.example/.well-known/web-identity:
{
  "provider_urls": ["https://idp.social.example/fedcm.json"]
}

The members of the issuer metadata are defined by Federated Credential Management API. This specification requires its provider_urls to contain exactly one entry: an https: URL, on the issuer’s site, of the issuer’s config file. § 6 Presentation derives the config file from it (create a registered provider).

To fetch the issuer metadata given a site host issuer and a globalObject, run the following steps. This returns issuer metadata or failure.
  1. Let metadata be the result of fetch the well-known file with a new URL whose scheme is "https", host is issuer, and path is « ".well-known", "web-identity" », and with globalObject. If metadata is failure, return failure.

  2. Set metadata’s issuer to issuer.

  3. Return metadata.

Should the issuer metadata include an issuer member that MUST equal the site it was fetched from, as in the Email Verification Protocol (following OAuth 2.0 Authorization Server Metadata)?

3.3. The Handle Registry

Each user agent has a global, persistent handle registry, an initially empty ordered set of handles that the user uses to sign in. No two handles in the handle registry match.

A handle is added to the handle registry in the same way whether the IDP registered it with register() or the user typed it (type your handle). The user agent doesn’t record which one did.

The handle registry stores only handles. It doesn’t store which IDP a handle resolves to, that IDP’s config file, or any accounts. The user agent computes the IDP from the handle by resolving it whenever it needs it, and obtains accounts from the IDP when needed.

Note: A user agent can cache the results of resolution and fetch the config file, for example to avoid DNS lookups and fetches on every federation request. That’s an implementation choice, as long as the cache respects DNS TTLs and HTTP caching, and is cleared along with the handle registry. See also § 8 Privacy considerations and § 9 Security considerations.

When the user clears browsing data or site settings for an origin origin, the user agent MUST remove from the handle registry every handle whose issuer, as determined by resolve the issuer (or by the user agent’s cached result of it), is on the same site as origin.

To add to the handle registry given a handle handle, run the following steps:
  1. If the handle registry contains a handle that matches handle, return.

  2. Append handle to the handle registry.

To remove from the handle registry given a handle handle, remove from the handle registry every handle that matches handle.

The user agent SHOULD let the user see the handle registry and remove handles from it (by running remove from the handle registry), for example to remove a mistyped handle.

Note: The handle registry is deliberately separate from the login status (and from the account registry, when supported). The login status records whether the user is signed in right now, which changes silently, while the handle registry records which handles the user uses, which the user approved and which outlives their session. After the user signs out, the IDP of a registered handle is still offered, the same way Federated Credential Management API offers an IDP that the user is logged-out of, so the user can sign back in. So an IDP calls setStatus("logged-out") when the user signs out, and unregister() only when the handle should no longer be offered, for example when the account is deleted.

4. IdP-initiated Registration

This section defines how an IDP registers and unregisters a user’s handle, and what it declares so that the user agent can present it to RPs later (§ 6 Presentation).

4.1. Registering a handle

partial interface IdentityProvider {
  static Promise<undefined> register(USVString handle);
  static Promise<undefined> unregister(USVString handle);
};

An IDP registering a handle and the user typing it (§ 5 User-initiated Registration) follow the same path: both resolve it, and both add the same handle to the handle registry.

Note: An IDP can only register and unregister handles that it is the issuer of, as named by the delegation record on the handle’s site.

A globalObject is on the issuer’s site of issuer metadata metadata if globalObject’s associated Document’s origin is not an opaque origin, and the site of its host is metadata’s issuer.
When the static register(handle) method is invoked, run the following steps:
  1. Let globalObject be the current global object.

  2. Let promise be a new Promise in globalObject’s relevant realm.

  3. If globalObject’s navigable is not a top-level traversable, reject promise with a "NotAllowedError" DOMException and return promise.

  4. In parallel, run the following steps:

    1. Let canonical be the result of parse a handle with handle. If canonical is failure, queue to reject promise with "SyntaxError" and return.

    2. Let metadata be the result of resolve a handle with canonical and globalObject. If metadata is failure, queue to reject promise with "NetworkError" and return.

    3. If globalObject is not on the issuer’s site of metadata, queue to reject promise with "NotAllowedError" and return.

    4. Let provider be the result of create a registered provider with metadata, « canonical », and null. If provider is failure, queue to reject promise with "NetworkError" and return.

    5. Let config be the result of fetch the config file with provider and globalObject.

    6. If config is failure, or config.federations doesn’t exist or is empty, queue to reject promise with "NetworkError" and return.

    7. Let accounts be the result of get the registered accounts with config, provider, and globalObject.

    8. If accounts is empty, queue to reject promise with "NotAllowedError" and return.

      Note: This fails if the user isn’t logged-in to the IDP, if fetching accounts_endpoint fails, or if none of the returned accounts has canonical as its handle.

    9. If the handle registry doesn’t already contain a handle that matches canonical, the user agent MUST ask the user to save canonical with accounts[0] and config. If the user declines, queue to reject promise with "NotAllowedError" and return.

    10. Add to the handle registry with canonical.

    11. Queue a global task on the DOM manipulation task source given globalObject to resolve promise.

  5. Return promise.

To ask to save a handle given a handle handle, an IdentityProviderAccount account, and an IdentityProviderAPIConfig config, the user agent shows a prompt that asks the user whether to save handle so that it’s offered when websites ask them to sign in. It returns whether the user agreed.

Note: An IDP sets its login status to logged-in before calling register(), as in the introduction’s example. This way, an IDP can only register a handle for an account the user is signed in to, and the prompt can show that account’s name and picture.

Note: register() is separate from setStatus() because a registered handle needs to outlive the user’s session. See § 3.3 The Handle Registry.

When the static unregister(handle) method is invoked, run the following steps:
  1. Let globalObject be the current global object.

  2. Let promise be a new Promise in globalObject’s relevant realm.

  3. In parallel, run the following steps:

    1. Let canonical be the result of parse a handle with handle. If canonical is failure, queue to reject promise with "SyntaxError" and return.

    2. Let metadata be the result of resolve a handle with canonical and globalObject. If metadata is failure, queue to reject promise with "NetworkError" and return.

    3. If globalObject is not on the issuer’s site of metadata, queue to reject promise with "NotAllowedError" and return.

    4. Remove from the handle registry with canonical.

    5. Queue a global task on the DOM manipulation task source given globalObject to resolve promise.

  4. Return promise.

unregister() removes the handle whether the IDP registered it or the user typed it: either way, the handle’s issuer has said it should no longer be offered. It resolves the same way whether or not the handle was in the handle registry, so that it doesn’t reveal whether the user had registered it.

Note: If a handle no longer resolves to the caller, for example because its delegation record was removed, the caller can’t unregister it. It doesn’t need to, because the handle is no longer offered with the caller as its issuer either (gather registered providers resolves it again).

4.2. Account handles

partial dictionary IdentityProviderAccount {
  USVString handle;
};
handle, of type USVString

The account’s handle, in the canonical form that parse a handle produces. The user agent uses it to match the account with registered and typed handles (find matching accounts).

Note: This is separate from username, which an IDP can use for any display name it likes. An account can have both.

To find matching accounts given a list of IdentityProviderAccounts accounts and a handle handle, return the items of accounts whose handle exists and matches handle.
Change the definition of displayable so that an account with its handle set is also displayable.

4.3. Declaring supported federations

partial dictionary IdentityProviderAPIConfig {
  sequence<USVString> federations;
};
federations, of type sequence<USVString>

The federation URLs that the IDP supports. An IDP that doesn’t list a federation is never returned for a federation request for that federation.

An IDP’s config file at https://idp.example/fedcm.json declares that it supports both IndieAuth and AT Protocol:
{
  "accounts_endpoint": "/accounts",
  "id_assertion_endpoint": "/assertion",
  "login_url": "/login",
  "federations": [
    "https://www.w3.org/TR/indieauth/",
    "https://atproto.com"
  ]
}

5. User-initiated Registration

A user whose handle isn’t in the handle registry yet can register it by typing it. The user agent decides when to allow users to type your handle, for example when an RP makes a federation request (§ 6.3 Account selection). The user agent checks that the typed handle resolves to an issuer that supports one of the federations the caller accepts, and adds it to the handle registry.

To type your handle given a list of federation URLs federations and a globalObject, run the following steps. This returns a handle or null.
  1. Show UI that asks the user to type a handle. The user agent MAY offer suggestions from its own local data (such as the handles in the handle registry). It MUST NOT send network requests while the user types.

  2. Wait for the user to submit a string input, or to cancel. If the user cancels, return null.

  3. Let handle be the result of check a typed handle with input, federations, and globalObject.

  4. If handle is failure, tell the user that no account provider was found for input, and go back to the first step.

  5. Add to the handle registry with handle.

    Note: Submitting the handle has the same effect as agreeing to save it after register(). The choice is stored before sign-in, so that it’s remembered even if sign-in fails or the user abandons it.

  6. Return handle.

To check a typed handle given a string input, a list of federation URLs federations, and a globalObject, run the following steps. This returns a handle or failure.
  1. Let handle be the result of parse a handle with input. If handle is failure, return failure.

  2. Let metadata be the result of resolve a handle with handle and globalObject. If metadata is failure, return failure.

  3. Let provider be the result of create a registered provider with metadata, « handle », and null. If provider is failure, return failure.

  4. Let config be the result of fetch the config file with provider and globalObject. If config is failure, return failure.

  5. If config.federations doesn’t exist, or none of its items is contained in federations, return failure.

  6. Return handle.

Note: Type your handle doesn’t deal with accounts or with the RP’s request. It only registers a handle. Showing the handle’s accounts and signing in with it is up to the caller (§ 6.3 Account selection).

6. Presentation

This section defines how the user agent presents registered IDPs to the user when an RP calls navigator.credentials.get() with a federation.

6.1. Requesting by federation

partial dictionary IdentityProviderConfig {
  USVString federation;
};
federation, of type USVString

A URL that points to the definition of a federation the RP accepts: the protocol or profile that its IDPs and RPs follow, for example "https://www.w3.org/TR/indieauth/" or "https://atproto.com". The authority behind the URL defines the federation. When present, configURL is ignored, and the entry stands for every registered IDP whose config file lists this federation.

An IdentityProviderRequestOptions whose federation exists is a federation request.

In Federated Credential Management API, remove the required keyword from the configURL and clientId members of IdentityProviderConfig.

Every algorithm in Federated Credential Management API that reads configURL from an IdentityProviderConfig passed by the RP MUST first check that the member exists, and treat its absence as failure. The one exception is create an IdentityCredential, as modified below.

In create an IdentityCredential, insert the following steps immediately after the step "Let mediation be options’s mediation":
  1. Let federationRequests be the items of providerList that are federation requests.

  2. If federationRequests’s size is greater than 1, return (failure, true).

  3. Remove the items of federationRequests from providerList.

  4. Let federationRequest be federationRequests[0] if it exists, and null otherwise.

  5. Let federationBranding be null.

  6. If federationRequest is not null:

    1. Let registered be the result of gather registered providers with federationRequest, providerList, and globalObject.

    2. Extend providerList with registered.

    3. If providerList’s size is greater than 1, set the offered as a choice flag of each item of registered.

    4. Set federationBranding to the result of get the federation branding with federationRequest.federation and globalObject.

    Note: The user agent can fetch the operator metadata in parallel with gather registered providers. federationBranding is used when building the UI (§ 6.3 Account selection).

In fetch an identity assertion, change the entry ("client_id", provider’s clientId) in the "Create a list" step to:
  1. ("client_id", clientId), where clientId is provider’s clientId if it exists, and otherwise the serialization of globalObject’s associated Document’s origin.

Is the RP’s origin the right default client_id for registered IDPs? It matches how IndieAuth identifies clients by URL, but other protocols may expect something else.

Note: configURL tells the RP which IDP issued the token, so it can verify the token using the protocol named by federation.

6.2. Registered providers

A registered provider is an IdentityProviderRequestOptions that the user agent creates for the IDP of one or more handles, rather than one the RP passed. The user agent keeps the following associated with it:

config

The IdentityProviderAPIConfig from fetch the config file.

handles

A list of handles that resolve to this IDP: the ones in the handle registry, or the single handle the user typed or the IDP is registering.

accounts

A list of IdentityProviderAccounts: the accounts that match one of its handles.

offered as a choice

A flag, initially unset. It is set when the request has more than one IDP, so that the user chooses which IDP to sign in to, rather than the user agent opening the login dialog of each IDP the user is logged-out of.

To create a registered provider given issuer metadata metadata, a list of handles handles, and an IdentityProviderRequestOptions or null federationRequest, run the following steps. This returns an IdentityProviderRequestOptions or failure.
  1. If metadata["provider_urls"] doesn’t exist or its size is not 1, return failure.

  2. Let configUrl be the result of running the URL parser on metadata["provider_urls"][0].

  3. If configUrl is failure, or configUrl’s scheme is not "https", or configUrl’s host’s registrable domain is not metadata’s issuer, return failure.

  4. Let provider be a new IdentityProviderRequestOptions.

  5. Set provider.configURL to the serialization of configUrl.

  6. If federationRequest is not null, then for each member name m of « clientId, params, fields, loginHint, domainHint »: if federationRequest[m] exists, set provider[m] to federationRequest[m].

  7. Mark provider as a registered provider, and set its handles to handles.

  8. Return provider.

Note: This is the only place where the user agent derives the issuer’s config file from its issuer metadata.

Because a registered provider requires exactly one provider_urls entry in the issuer metadata, a site can register handles for only one config file. Is that acceptable for IDPs that have several configurations (e.g. one per product)?

To get the registered accounts given an IdentityProviderAPIConfig config, a registered provider provider, and a globalObject, run the following steps. This returns a list of IdentityProviderAccounts.
  1. Let origin be the origin of the result of running the URL parser on provider’s configURL.

  2. If get the login status for origin is not logged-in, return an empty list.

  3. Return the result of fetch accounts with config, provider, and globalObject.

    Note: fetch accounts only returns the accounts that match one of provider’s handles (see below).

In filter accounts, insert the following step before the step that returns accountList:
  1. If provider is a registered provider, remove from accountList every account whose handle doesn’t exist or doesn’t match one of provider’s handles.

Note: This filters the accounts the same way as loginHint. Like the other filters, it runs after the account pictures are fetched, so that the IDP can’t tell from the picture fetches which accounts were kept.

Note: Only accounts whose handle is a registered handle are shown to other RPs, because those are the accounts the user agreed to (with register(), or by typing the handle). An IDP’s accounts_endpoint can return other accounts (or push them via Account Registry API), but they aren’t shown on a federation request until their handles are registered.

Should other accounts from a registered IDP also be shown, once the user has registered at least one handle with it?

To group registered handles by issuer given a globalObject, run the following steps. This returns an ordered map from site hosts to tuples of (issuer metadata, a list of handles).
  1. Let groups be an empty ordered map.

  2. For each handle of the handle registry:

    1. Let metadata be the result of resolve a handle with handle and globalObject. If metadata is failure, continue.

    2. Let issuer be metadata’s issuer.

    3. If groups[issuer] doesn’t exist, set groups[issuer] to (metadata, an empty list).

    4. Append handle to groups[issuer]'s second item.

  3. Return groups.

Note: Several handles can resolve to the same IDP, for example when two users of the same IDP share a user agent. Grouping them means the user agent fetches that IDP’s config file and accounts once.

To gather registered providers given an IdentityProviderRequestOptions federationRequest, a list of IdentityProviderRequestOptions explicitProviders, and a globalObject, run the following steps. This returns a list of registered providers.
  1. Let seenOrigins be the ordered set of the origins of the configURLs of explicitProviders.

  2. Let result be an empty list.

  3. For each (metadata, handles) of the values of the result of group registered handles by issuer with globalObject:

    1. Let provider be the result of create a registered provider with metadata, handles, and federationRequest. If provider is failure, continue.

    2. Let origin be the origin of the result of running the URL parser on provider.configURL.

    3. If seenOrigins contains origin, continue.

      Note: If the RP also lists this IDP explicitly, the explicit entry is used and the registration is ignored, so that the same IDP doesn’t appear twice.

    4. Append origin to seenOrigins, and append provider to result.

  4. Return result.

Note: Gather registered providers doesn’t fetch anything from the IDPs. Create an IdentityCredential fetches each registered provider’s config file and accounts like any other IDP’s.

To load a registered provider given a registered provider provider, a federation request federationRequest, and a globalObject, run the following steps. This returns success or failure.
  1. Let config be the result of fetch the config file with provider and globalObject.

  2. If config is failure, return failure.

  3. If config.federations doesn’t exist or doesn’t contain federationRequest.federation, return failure.

  4. Set provider’s config to config, and its accounts to the result of get the registered accounts with config, provider, and globalObject.

  5. Return success.

Note: Fetch the config file checks a registered provider’s config file against the well-known file, as it does for any other IDP. The check passes as long as the well-known file hasn’t changed since resolution, because the user agent obtained the config URL from that same well-known file. The user agent can reuse the well-known file it fetched during resolution, subject to HTTP caching.

In create an IdentityCredential, in the step "For each provider in providerList", insert the following step immediately after the step "If config is failure, continue":
  1. If provider is a registered provider, and config.federations doesn’t exist or doesn’t contain federationRequest.federation, continue.

    Note: Otherwise, a registered provider goes through these steps like any other IDP: the user agent checks its login status, fetches its config file and accounts (keeping only the accounts that match its handles), and shows the same UI as Federated Credential Management API does when the user is logged-out or when no account matches.

In handle a logged-out IDP, insert the following step at the beginning:
  1. If provider is a registered provider whose offered as a choice flag is set, return "logged-out".

In create an IdentityCredential, in the "Build UI" step, when the value for a registered provider is "logged-out" and mode is active, the user agent MUST prompt the user whether to continue with this IDP, rather than adding nothing or returning (failure, false).

Note: When a request has more than one IDP, each IDP that the user is logged-out of is then offered as a choice next to the accounts of the IDPs the user is signed in to, and only the one the user chooses opens its login dialog. With a single IDP, Federated Credential Management API’s behavior is unchanged.

6.3. Account selection

A handle chip for a handle handle, a registered provider provider, and an IdentityProviderBranding or null federationBranding is a user agent UI element that represents an account the user hasn’t signed in to yet:

An account from find matching accounts is shown the same way FedCM shows accounts. If federationBranding is not null, the user agent MAY add its icon as a badge.

The user agent MAY also use federationBranding elsewhere in the dialog, for example to title it with the federation’s name ("Sign in with your Atmosphere account"), and in the UI that allows users to type your handle.

To sign in with a handle given a handle handle, a registered provider provider, and a globalObject, run the following steps. This returns an IdentityProviderAccount or failure.
  1. Let loginProvider be a copy of provider. If loginProvider’s loginHint is empty, set it to handle.

    Note: Show an IDP login dialog sends loginProvider’s loginHint to the IDP as login_hint. Only the login dialog uses loginProvider, so that the handle doesn’t also filter the accounts that are fetched afterwards.

  2. Let result be the result of show an IDP login dialog with provider’s config, loginProvider, and globalObject.

  3. If result is failure, return failure.

  4. Let matches be the result of find matching accounts with the result of get the registered accounts with provider’s config, provider, and globalObject, and with handle.

  5. If matches’s size is 1, return matches[0].

  6. If matches’s size is greater than 1, let the user choose one of matches and return it. If the user cancels, return failure.

  7. Tell the user that the account they signed in with doesn’t match handle, and return failure.

In create an IdentityCredential:

Change the step "If providerMap is empty, return (failure, false)" to:

  1. If providerMap is empty:

    1. If federationRequest is null, or mode is not active, return (failure, false).

    2. Otherwise, the user agent MAY continue, only allowing users to type your handle.

After the "Build UI" step, add the following step:

  1. If federationRequest is not null, the user agent SHOULD allow users to type your handle. If the user chooses to:

    1. Let handle be the result of type your handle with « federationRequest.federation » and globalObject. If handle is null, return to the dialog.

    2. Let metadata be the result of resolve a handle with handle and globalObject. If metadata is failure, return to the dialog.

    3. Let provider be the result of create a registered provider with metadata, « handle », and federationRequest. If provider is failure, return to the dialog.

    4. If load a registered provider with provider, federationRequest, and globalObject returns failure, return to the dialog.

    5. If provider’s accounts is not empty, show each of them. Otherwise, show a handle chip for handle, provider, and federationBranding.

    6. Wait for the user to select one of them, or to cancel. If the user cancels, return to the dialog.

    7. If the user selected an account, let account be that account. Otherwise, let account be the result of sign in with a handle with handle, provider, and globalObject, and if account is failure, return to the dialog.

    8. Set selectedAccount to account and permission to true, set the relevant provider to provider and the relevant config to provider’s config, and close the dialog.

    Note: The user agent can reuse the results of resolution and fetch the config file from type your handle, rather than repeating them.

7. Requirements on other parties

This section is non-normative.

Party What it does
IDP Lists the federations it supports in federations. Serves a well-known file at its site with a single provider_urls entry. Sets each account’s handle to the user’s handle in its accounts_endpoint response (or when pushing accounts with Account Registry API). After the user signs in, sets its login status to logged-in and calls register() with that handle. Calls unregister() when the handle stops existing (e.g. the account is deleted or renamed), and setStatus("logged-out") when the user merely signs out. Accepts login_hint on its login_url.
Owner of a handle’s site Publishes a delegation record naming the issuer.
Federation operator Serves operator metadata at /.well-known/web-identity-federation on the origin of its federation URL, including the federation’s branding.
RP Passes a federation, and verifies the returned token according to that federation, including checking that it was issued for the handle and by the configURL’s IDP.

8. Privacy considerations

Request When Credentials Reveals the RP?
Operator metadata of the requested federation On a federation request, unless cached No No
Config file of registered IDPs On a federation request No No
accounts_endpoint of registered IDPs On a federation request, when the IDP’s login status is logged-in Yes No
DNS TXT lookup and well-known file, for each registered handle On a federation request, unless cached No No
DNS TXT lookup, well-known file, config file, and (when logged-in) accounts_endpoint of the resolved IDP After the user submits a handle, or when an IDP calls register() or unregister() Only for accounts_endpoint No
login_url After the user chooses to sign in to an IDP they are logged-out of, or selects a handle chip Yes (a top-level navigation) No
id_assertion_endpoint After the user selects an account Yes Yes

9. Security considerations

10. Examples

This section is non-normative.

**The handle and the IDP are on the same site.** Alice runs her own atproto server at pds.alice.example, and her handle is @alice.example.
  1. Alice chooses to type your handle and types @alice.example. Parse a handle gives @alice.example, whose site is alice.example.

  2. _web-identity.alice.example has TXT "iss=alice.example", so resolve the issuer returns alice.example.

  3. https://alice.example/.well-known/web-identity returns {"provider_urls": ["https://pds.alice.example/config.json"]}, which is on the same site.

  4. The config file lists https://atproto.com in federations, so the user agent adds @alice.example to the handle registry.

  5. Alice isn’t signed in to pds.alice.example yet, so the user agent shows a handle chip for @alice.example.

  6. When Alice selects it, the user agent opens login_url?login_hint=%40alice.example. Her server signs her in, its accounts_endpoint returns an account with {handle: "@alice.example"}, and the user agent requests the token.

**The handle and the IDP are on different sites.** Bob’s handle is @bob.example, and his account is hosted by social.example.
  1. Bob publishes _web-identity.bob.example TXT "iss=social.example".

  2. Resolve the issuer returns social.example. The user agent fetches https://social.example/.well-known/web-identity and then its config file.

  3. The user agent shows Bob’s account with handle: "@bob.example" if he is signed in to social.example, and otherwise offers social.example as Federated Credential Management API does for an IDP the user is logged-out of.

Because @bob.example resolves to social.example, social.example can also call IdentityProvider.register("@bob.example") after Bob signs in there. Another site, such as evil.example, can’t: the handle doesn’t resolve to it.

The handle is a subdomain of the provider. Carol’s handle is @carol.social.example.
  1. Its site is social.example, which publishes _web-identity.social.example TXT "iss=social.example", so resolve the issuer returns social.example.

  2. Resolution continues as in the previous example.

Many users share a host. Dave (dave@social.example) and Erin (@erin.social.example) both resolve to the same issuer, social.example. The handle registry has both handles, and group registered handles by issuer groups them together. On a federation request, the user agent fetches that config file and accounts_endpoint once, and shows the signed-in accounts that match dave@social.example or @erin.social.example.
Signing out versus deleting an account. Erin registered @erin.social.example from social.example.
  1. Erin signs out of social.example, which calls navigator.login.setStatus("logged-out"). @erin.social.example stays in the handle registry. On the next federation request, the user agent sees that social.example is logged-out and offers it as Federated Credential Management API does for an IDP the user is logged-out of. Signing in opens social.example’s login_url.

  2. Later, Erin deletes her account. social.example calls IdentityProvider.unregister("@erin.social.example"), and the handle is removed from the handle registry, so the handle is no longer offered to any RP.

11. Acknowledgements

Many thanks to Aaron Parecki, Anders Pitman, Emelia Smith, Nicolás Peña Moreno, Dick Hardt, and the IndieWeb, Solid, atproto and Fediverse communities for their feedback on this proposal.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. Key words for use in RFCs to Indicate Requirement Levels

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CREDENTIAL-MANAGEMENT-1]
Nina Satragno; Marcos Caceres. Credential Management Level 1. URL: https://w3c.github.io/webappsec-credential-management/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FEDCM]
Nicolas Pena Moreno. Federated Credential Management API. URL: https://w3c-fedid.github.io/FedCM/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[LOGIN-STATUS]
Login Status API. Editor's Draft. URL: https://w3c-fedid.github.io/login-status/
[RFC1035]
P. Mockapetris. Domain names - implementation and specification. November 1987. Internet Standard. URL: https://www.rfc-editor.org/info/rfc1035/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Non-Normative References

[ACCOUNT-REGISTRY]
Sam Goto. Account Registry API. CG-DRAFT. URL: https://w3c-fedid.github.io/idp-registration/account-registry.html
[RFC8414]
M. Jones; N. Sakimura; J. Bradley. OAuth 2.0 Authorization Server Metadata. June 2018. Proposed Standard. URL: https://www.rfc-editor.org/info/rfc8414/

IDL Index

dictionary IdentityFederationOperatorMetadata {
  IdentityProviderBranding branding;
};

partial interface IdentityProvider {
  static Promise<undefined> register(USVString handle);
  static Promise<undefined> unregister(USVString handle);
};

partial dictionary IdentityProviderAccount {
  USVString handle;
};

partial dictionary IdentityProviderAPIConfig {
  sequence<USVString> federations;
};

partial dictionary IdentityProviderConfig {
  USVString federation;
};

Issues Index

Because the operator metadata lives at a well-known path on the federation URL’s origin, one origin can operate only one federation. Should the federation URL point at the operator metadata directly instead? ↵
URL handles need more normalization (fragments such as #me, trailing slashes). ↵
Should the record name be _web-identity or something more specific (e.g. _web-handle)? Should it carry a federation, so that one domain can delegate different protocols to different IDPs? ↵
Should the issuer metadata include an issuer member that MUST equal the site it was fetched from, as in the Email Verification Protocol (following OAuth 2.0 Authorization Server Metadata)? ↵
Is the RP’s origin the right default client_id for registered IDPs? It matches how IndieAuth identifies clients by URL, but other protocols may expect something else. ↵
Because a registered provider requires exactly one provider_urls entry in the issuer metadata, a site can register handles for only one config file. Is that acceptable for IDPs that have several configurations (e.g. one per product)? ↵
Should other accounts from a registered IDP also be shown, once the user has registered at least one handle with it? ↵