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 pushes the user’s account to the user agent (see § 4 The Account Registry) and then 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 (find your account).

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 pushes her account and registers her handle:
await navigator.login.setStatus("logged-in", {
  accounts: [{
    id: "1234",
    name: "Alice",
    handle: "@alice.idp.example",
    picture: "https://idp.example/alice.jpg"
  }]
});

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:

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

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

A change to an algorithm or definition in another specification.

2. Infrastructure

This specification depends on the Infra Standard. Infra Standard

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.

3. Handles

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.

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 domain of a handle handle is the result of the following steps:
  1. If handle starts with "https://", return the host of the result of running the URL parser on handle.

  2. Return the result of running the host parser on the part of handle after the last "@".

Note: Because a handle is always in the canonical form that parse a handle produces, its domain can be read off directly, without validating it again.

Input Handle Its domain
@alice.example, alice.example @alice.example alice.example
@alice.social.example @alice.social.example alice.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 the config URL of its issuer.

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

  2. Let wellKnown be the result of fetch the well-known file with issuer and globalObject. If wellKnown is failure, return failure.

  3. If wellKnown["provider_urls"] doesn’t exist or its size is not 1, return failure.

  4. Let url be the result of running the URL parser on wellKnown["provider_urls"][0].

  5. If url is failure, or url’s scheme is not "https", or url’s host’s registrable domain is not issuer, return failure.

  6. Return url.

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 config URL: 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. It is based on one rule: **a handle’s issuer is on the handle’s own site, unless the handle’s domain delegates to another site in DNS.** There are three cases:

The handle’s own site hosts it

The issuer of @alice.example is on alice.example. Alice runs her own IDP, or has a provider serve the well-known file on her site for her. No DNS record is needed.

The handle’s domain delegates to another issuer site

The issuer of @alice.example is on social.example. Alice keeps her domain as her handle but uses a provider as its issuer, so she publishes a delegation record on alice.example naming social.example.

The provider assigned the handle under its own domain

The issuer of @alice.social.example and of alice@social.example is on social.example. This is really the first case: the handle’s own site is social.example, because a site is a registrable domain. No DNS record is needed.

In every case, the user agent first looks for a delegation record on the handle’s domain. If there is one, its iss= value names the issuer’s site. If there isn’t, the issuer is on the handle’s own 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 domain be handle’s domain.

  2. Let records be the result of look up the delegation records for domain.

  3. If records is failure or is empty, return domain’s registrable domain.

  4. If records’s size is greater than 1, return failure.

  5. Let issuer be the result of running the host parser on the ASCII lowercased records[0].

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

  7. Return issuer.

Note: When the provider’s domain is a public suffix (for example, hosting.example, where each user gets their own subdomain), each subdomain is its own site. @alice.hosting.example falls under the first case, and its issuer is on alice.hosting.example, not on hosting.example.

Should a failed DNS query (e.g. SERVFAIL) fail closed instead of falling back to the handle’s own site?

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

3.2.2. DNS delegation record

The owner of a handle’s domain can delegate to an issuer on another site by publishing a DNS TXT record Domain names - implementation and specification at the name _web-identity. followed by the handle’s host:

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

This record says that the issuer of handles on alice.example is on the site social.example.

The record’s value MUST be the string "iss=" followed by a site host. There MUST be at most one such record at a name. A record naming the handle’s own site has the same effect as having no record.

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 domain host, the user agent performs a DNS query for TXT records at the name _web-identity. followed by host, using its DNS resolver. This returns a list of strings (the records whose contents start with "iss=", with that prefix removed), or failure if the query failed.

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 (find your account). 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 accounts come from the account registry.

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 account registry and the login status. Those record 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, a registered handle is still offered as a handle chip that signs the user 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. The Account Registry

Accounts push is described in Lightweight FedCM but isn’t specified yet. This section defines the minimum this specification needs, and should move to Login Status API or Federated Credential Management API once accounts push is specified there.

Each user agent has a global, persistent account registry, which stores the accounts that IDPs push with setStatus(). It is an initially empty map. Its keys are origins of IDPs, and its values are account registry entries. An account registry entry is a struct with the following items:

accounts

A list of IdentityProviderAccounts.

expiration

A moment in time, or null for no expiration.

dictionary LoginStatusOptions {
  sequence<IdentityProviderAccount> accounts;
  [EnforceRange] unsigned long long expiration;
};
In Login Status API, change the setStatus() method so that it takes an optional second argument, optional LoginStatusOptions options = {}, and add the following steps at the end of its algorithm:
  1. If status is "logged-out", remove account registry[origin] and return.

  2. If options["accounts"] does not exist, return.

  3. For each account of options["accounts"]: if account has none of name, email, tel, username, or handle, throw a TypeError.

  4. Let expiration be null.

  5. If options["expiration"] exists, set expiration to the current time plus options["expiration"] milliseconds.

  6. Set account registry[origin] to a new struct with accounts options["accounts"] and expiration expiration.

Also, in the algorithm that processes the Set-Login header, when the token is "logged-out", remove account registry[origin].

To get the pushed accounts for an origin origin, run the following steps. This returns a list of IdentityProviderAccounts.
  1. If account registry[origin] does not exist, return an empty list.

  2. Let entry be account registry[origin].

  3. If entry’s expiration is not null and is in the past, remove account registry[origin] and return an empty list.

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

  5. Return entry’s accounts.

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.

The user agent MUST clear account registry entries whenever it clears the corresponding Login Status map entries.

5. 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).

5.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 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. An IDP on another site is a handle’s issuer only if the handle’s domain delegates to it with a delegation record.

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 configUrl be the result of resolve a handle with canonical and globalObject. If configUrl is failure, queue to reject promise with "NetworkError" and return.

    3. If configUrl’s origin is not same site with globalObject’s associated Document’s origin, queue to reject promise with "NotAllowedError" and return.

    4. Let accounts be the result of find matching accounts with the result of get the pushed accounts for configUrl’s origin, and canonical.

    5. 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 the IDP hasn’t pushed any accounts, or if none of them has canonical as its handle.

    6. Let provider be the result of create a registered provider with configUrl and null.

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

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

    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 pushes the user’s account with setStatus() 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 configUrl be the result of resolve a handle with canonical and globalObject. If configUrl is failure, queue to reject promise with "NetworkError" and return.

    3. If configUrl’s origin is not same site with globalObject’s associated Document’s origin, 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).

5.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.

5.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.

In Federated Credential Management API, remove the required keyword from the accounts_endpoint member of IdentityProviderAPIConfig. In fetch the config file, if config.accounts_endpoint doesn’t exist, set accounts_url to null instead of computing it. Treat a null accounts_url as failure, except for registered providers.

Note: Registered IDPs only need a config file and an id_assertion_endpoint. Their accounts are always pushed.

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. It changes Federated Credential Management API as follows, using the algorithms defined in the subsections 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. 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.

Then, in the step "For each provider in providerList", insert the following as the first sub-step:

  1. If provider is a registered provider:

    1. Set providerMap[providerOrigin] to provider’s accounts.

    2. Continue.

    Note: For registered providers, the user agent doesn’t check the login status, fetch the accounts_endpoint, or show mismatch UI, because it already knows the accounts that were pushed.

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, showing only the find your account affordance.

In the "Build UI" step, add the following for each registered provider provider in providerList:

  1. For each handle handle of provider’s handles for which find matching accounts with provider’s accounts and handle returns an empty list, add a handle chip for handle and provider. If the user selects it:

    1. Let account be the result of sign in with a handle with handle, provider, and globalObject.

    2. If account is not failure, 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.

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

  1. If federationRequest is not null, the user agent SHOULD add a find your account affordance. If the user triggers it:

    1. Let result be the result of find your account with federationRequest and globalObject.

    2. If result is not null, let (account, resolvedProvider) be result. Set selectedAccount to account and permission to true, set the relevant provider to resolvedProvider and the relevant config to resolvedProvider’s config, and close the dialog.

Add the following algorithm:
To fetch the well-known file given a site host site and a globalObject, run the steps of fetch the config file that create wellKnownRequest and fetch it, with rootUrl set to a new URL with scheme "https", host site, and path « ".well-known", "web-identity" », and with globalObject. Return the resulting wellKnown, an IdentityProviderWellKnown or failure.

Then, in fetch the config file, replace those steps with a call to fetch the well-known file with rootUrl’s host and globalObject.

Note: This only moves existing steps, so that resolution can reuse them. It doesn’t change the behavior of fetch the config file.

In fetch the config file, insert the following step immediately before the step "If rpOrigin is not an opaque origin, and ...":
  1. If provider is a registered provider, set skipWellKnown to true.

Note: The well-known file exists to stop an RP from encoding tracking information in the config URL it passes. A registered provider’s config URL doesn’t come from the RP. The user agent obtained it from the well-known file itself, by resolving a handle.

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

In show an IDP login dialog, insert the following step immediately after the step that appends login_hint:
  1. If provider is a registered provider with an associated handle handle, and provider’s loginHint is empty, append ("login_hint", handle) to queryList.

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.1. Requesting by federation

partial dictionary IdentityProviderConfig {
  USVString federation;
};
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 in § 6 Presentation.

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.

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.

accounts

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

associated handle

The handle the user chose to sign in with, or null. Show an IDP login dialog sends it as login_hint.

To create a registered provider given a URL configUrl and an IdentityProviderRequestOptions or null federationRequest, run the following steps. This returns an IdentityProviderRequestOptions.
  1. Let provider be a new IdentityProviderRequestOptions.

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

  3. 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].

  4. Mark provider as a registered provider.

  5. Return provider.

To get the registered accounts given a URL configUrl and a list of handles handles, run the following steps. This returns a list of IdentityProviderAccounts.
  1. Let pushed be the result of get the pushed accounts for configUrl’s origin.

  2. Let result be an empty list.

  3. For each handle of handles: extend result with the items of the result of find matching accounts with pushed and handle that result doesn’t already contain.

  4. Return result.

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 can push other accounts, but they aren’t shown on a federation request until their handles are registered.

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

To group registered handles by config URL given a globalObject, run the following steps. This returns an ordered map from URLs of config files to lists of handles.
  1. Let groups be an empty ordered map.

  2. For each handle of the handle registry:

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

    2. If groups[configUrl] doesn’t exist, set groups[configUrl] to an empty list.

    3. Append handle to groups[configUrl].

  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 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 federation be federationRequest.federation.

  2. Let seenOrigins be the ordered set of the origins of the configURLs of explicitProviders.

  3. Let result be an empty list.

  4. For each configUrl → handles of the result of group registered handles by config URL with globalObject:

    1. Let origin be configUrl’s origin.

    2. 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.

    3. Let provider be the result of create a registered provider with configUrl and federationRequest.

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

    5. If config is failure, continue.

    6. If config.federations doesn’t exist or doesn’t contain federation, continue.

    7. Let accounts be the result of get the registered accounts with configUrl and handles.

    8. For each account of accounts where account["picture"] exists, fetch the account picture with account and globalObject.

    9. Set provider’s config to config, its handles to handles, and its accounts to accounts.

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

  5. Return result.

6.3. Account selection

A handle chip for a handle handle and a registered provider provider 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. The user agent MAY add the federation branding as a badge.

The federation branding for a federation federation is a name and an icon published by the authority behind federation, or null. The user agent obtains it without credentials and MAY cache it.

Define where the federation authority publishes its branding, and in which format. [Issue #29]

To find your account given an IdentityProviderRequestOptions federationRequest and a globalObject, run the following steps. This returns a pair (an IdentityProviderAccount, a registered provider), 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 provider be the result of look up a typed handle with input, federationRequest, and globalObject.

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

  5. Let handle be provider’s handles[0].

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

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

  8. Add to the handle registry with handle.

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

  9. If the user selected an account account, return (account, provider).

  10. Let account be the result of sign in with a handle with handle, provider, and globalObject.

  11. If account is failure, go back to the first step.

  12. Return (account, provider).

To look up a typed handle given a string input, an IdentityProviderRequestOptions federationRequest, and a globalObject, run the following steps. This returns a registered provider or failure.
  1. Let handle be the result of parse a handle with input. If handle is failure, return failure.

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

  3. Let provider be the result of create a registered provider with configUrl and federationRequest.

  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 doesn’t contain federationRequest.federation, return failure.

  6. Set provider’s config to config, its handles to « handle », and its accounts to the result of get the registered accounts with configUrl and « handle ».

  7. Return provider.

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. Set provider’s associated handle to handle.

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

  3. If result is failure, return failure.

  4. Let matches be the result of get the registered accounts with the result of running the URL parser on provider’s configURL, and « 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.

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. Pushes accounts with setStatus(), setting each account’s handle to the user’s handle. After the user signs in, 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 domain, when its issuer is on another site Publishes a delegation record.
Owner of a handle’s domain, when its issuer is on the same site Serves the well-known file at the site. The config file can be on any same-site host.
Federation authority Publishes federation branding for its federation.
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?
Config file of registered IDPs On a federation request No 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 and config file of the resolved IDP After the user submits a handle, or when an IDP calls register() or unregister() No No
login_url After the user 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 types @alice.example in find your account. Parse a handle gives @alice.example, whose domain is alice.example.

  2. There’s no delegation record, 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.

  5. No account has been pushed yet, so the user agent shows a handle chip for @alice.example.

  6. When Alice selects it, the user agent adds @alice.example to the handle registry and opens login_url?login_hint=%40alice.example. Her server pushes {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 pushed account with handle: "@bob.example", or a handle chip.

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. There’s no record at _web-identity.carol.social.example, so resolve the issuer returns social.example, the registrable domain.

  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 config file. The handle registry has both handles, and group registered handles by config URL groups them together. On a federation request, the user agent fetches that config file once, and shows the pushed accounts that match dave@social.example or @erin.social.example, plus a handle chip for either handle that doesn’t have a matching pushed account.
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"). Her pushed account is cleared, but @erin.social.example stays in the handle registry. On the next federation request, the user agent shows a handle chip for @erin.social.example. Selecting it opens social.example’s login_url with login_hint=%40erin.social.example.

  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/

IDL Index

dictionary LoginStatusOptions {
  sequence<IdentityProviderAccount> accounts;
  [EnforceRange] unsigned long long expiration;
};

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

URL handles need more normalization (fragments such as #me, trailing slashes). ↵
Should a failed DNS query (e.g. SERVFAIL) fail closed instead of falling back to the handle’s own site? ↵
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? ↵
Accounts push is described in Lightweight FedCM but isn’t specified yet. This section defines the minimum this specification needs, and should move to Login Status API or Federated Credential Management API once accounts push is specified there. ↵
Because resolution requires exactly one provider_urls entry, a site can register handles for only one config file. Is that acceptable for IDPs that have several configurations (e.g. one per product)? ↵
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. ↵
Should other pushed accounts from a registered IDP also be shown, once the user has registered at least one handle with it? ↵
Define where the federation authority publishes its branding, and in which format. [Issue #29] ↵