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), and so that relying parties can request any identity provider that issues one of them and belongs to 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. Handles are parsed and resolved, and the relying party receives a verifiable proof that the user controls the handle, as defined by the Handle Verification Protocol Handle Verification Protocol.

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 belongs to a federation it accepts, by passing a federation instead of a configURL:

    const credential = await navigator.credentials.get({
      identity: {
        providers: [{
          federation: "https://atproto.com",
          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. 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.

This specification builds on the Handle Verification Protocol Handle Verification Protocol, which defines what a handle is, how it is resolved to the issuer that the user has authorized to vouch for it, and how the user agent obtains from that issuer a proof that the user controls the handle, an HVT+KB, which the RP verifies. The issuer doesn’t learn which RP the user signs in to.

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

The user agent resolves @alice.example, through its DID document, to its issuer, https://social.example. Its issuer metadata, at https://social.example/.well-known/handle-verification, lists the federations that its accounts belong to:

{
  "issuer": "https://social.example",
  "issuance_endpoint": "https://social.example/hvp/issuance",
  "jwks_uri": "https://social.example/hvp/jwks",
  "federations": ["https://atproto.com"]
}

The well-known file of its site, at https://social.example/.well-known/web-identity, tells the user agent where to fetch Alice’s accounts and where to let her sign in:

{
  "accounts_endpoint": "/accounts",
  "login_url": "/login"
}

And its accounts_endpoint includes her handle on her account:

{
  "accounts": [{
    "id": "1234",
    "name": "Alice",
    "handle": "@alice.example",
    "picture": "https://social.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 atproto, is identified by an https: URL: the URL that RPs pass as federation and that issuers list in the federations member of their issuer metadata. 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. It only reads the operator metadata, and matches the federation URL with the federations of issuers.

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. The Handle Registry

This specification depends on the Infra Standard. Infra Standard

Handle Verification Protocol defines the handles that the user agent remembers, how they are parsed and resolved, and the issuer metadata of the IDPs that issue them. This specification uses the following terms for them:

To resolve a handle given a handle handle and a globalObject, run the steps of HVP resolution on handle. If they fail, return failure. Otherwise, return a resolution result whose issuer is the authorized issuer that they produced, and whose unique user identifier is the unique user identifier that they produced.

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 issuer metadata, 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 the documents that create a registered provider fetches, for example to avoid lookups and fetches on every federation request. That’s an implementation choice, as long as the cache respects DNS TTLs and HTTP caching, follows the caching rules of Handle Verification Protocol, 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 a handle (or by the user agent’s cached result of it), is same site with 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 records that HVP resolution reads: the HandleVerification service of the handle’s DID document.

A globalObject is on the issuer’s site of a resolution result result if globalObject’s associated Document’s origin is not an opaque origin, and is same site with result’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 result be the result of resolve a handle with canonical and globalObject. If result is failure, queue to reject promise with "NetworkError" and return.

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

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

    5. If provider’s issuer metadata’s federations doesn’t exist or is empty, queue to reject promise with "NetworkError" and return.

    6. Let accounts be the result of get the registered accounts with provider’s config, provider, and globalObject.

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

    8. 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]. If the user declines, queue to reject promise with "NotAllowedError" and return.

    9. Add to the handle registry with canonical.

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

    3. If globalObject is not on the issuer’s site of result, 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 DID document no longer names the caller as its issuer, 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, for example "@alice.example". The user agent uses it to match the account with registered and typed handles (find matching accounts), and uses it in the HVT request (§ 6.4 Issuing the HVT+KB).

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.

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 belongs to 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 result be the result of resolve a handle with handle and globalObject. If result is failure, return failure.

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

  4. If provider’s issuer metadata’s federations doesn’t exist, or none of its items is contained in federations, return failure.

  5. 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, and what the RP receives.

6.1. Requesting by federation

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

A URL that identifies a federation the RP accepts, for example "https://atproto.com". When present, configURL is ignored, and the entry stands for every registered issuer whose issuer metadata lists this federation in its federations.

An IdentityProviderRequestOptions whose federation exists is a federation request. The RP passes the nonce that the HVT+KB is bound to as its nonce.

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

6.2. Registered providers

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

issuer

The HVP issuer identifier of its issuer.

issuer metadata

The issuer’s issuer metadata, from fetch the issuer metadata.

config

The issuer’s IdentityProviderAPIConfig, from fetch the issuer config. It has only an accounts_endpoint and a login_url.

handles

A list of handles that resolve to this issuer: 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 show the user’s accounts and to let the user sign in, the user agent needs the issuer’s accounts_endpoint and login_url. It reads them from the well-known file of Federated Credential Management API on the issuer’s site, rather than from a config file: a registered provider has no config URL.

The well-known file at https://social.example/.well-known/web-identity:
{
  "accounts_endpoint": "/accounts",
  "login_url": "/login"
}
To fetch the issuer config given an HVP issuer identifier issuer and a globalObject, run the following steps. This returns an IdentityProviderAPIConfig or failure.
  1. Let site be the registrable domain of issuer’s host. If site is null, return failure.

  2. Let wellKnownUrl be a new URL whose scheme is "https", host is site, and path is « ".well-known", "web-identity" ».

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

  4. Let config be a new IdentityProviderAPIConfig.

  5. For each member name m of « "accounts_endpoint", "login_url" »:

    1. If wellKnown[m] doesn’t exist, return failure.

    2. Let url be the result of running the URL parser on wellKnown[m], with wellKnownUrl as the base URL.

    3. If url is failure, or its scheme is not "https", or its origin is not same site with issuer, return failure.

    4. Set config[m] to the serialization of url.

  6. Return config.

In Federated Credential Management API, remove the required keyword from the id_assertion_endpoint member of IdentityProviderAPIConfig. A registered provider doesn’t use it (§ 6.4 Issuing the HVT+KB). Every algorithm in Federated Credential Management API that reads it from the config file of an IDP that isn’t a registered provider MUST first check that the member exists, and treat its absence as failure.
A registered provider has no configURL. In Federated Credential Management API:
To create a registered provider given an HVP issuer identifier issuer, a list of handles handles, an IdentityProviderRequestOptions or null federationRequest, and a globalObject, run the following steps. This returns an IdentityProviderRequestOptions or failure.
  1. Let metadata be the result of fetch the issuer metadata with issuer and globalObject. If metadata is failure, return failure.

  2. Let config be the result of fetch the issuer config with issuer and globalObject. If config is failure, return failure.

  3. Let provider be a new IdentityProviderRequestOptions.

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

  5. Mark provider as a registered provider, and set its issuer to issuer, its issuer metadata to metadata, its config to config, and its handles to handles.

  6. Return provider.

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. If get the login status for provider’s issuer is not logged-in, return an empty list.

  2. 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 HVP issuer identifiers to lists of handles.
  1. Let groups be an empty ordered map.

  2. For each handle of the handle registry:

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

    2. Let issuer be result’s issuer.

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

    4. Append handle to groups[issuer].

  3. Return groups.

Note: Several handles can resolve to the same issuer, for example when two users of the same IDP share a user agent. Grouping them means the user agent fetches that issuer’s issuer metadata 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 issuer → handles of the result of group registered handles by issuer with globalObject:

    1. If seenOrigins contains issuer, 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.

    2. Let provider be the result of create a registered provider with issuer, handles, federationRequest, and globalObject. If provider is failure, continue.

    3. If provider’s issuer metadata’s federations doesn’t exist or doesn’t contain federationRequest.federation, continue.

    4. Append provider to result.

  4. Return result.

Note: Gather registered providers fetches each issuer’s issuer metadata and well-known file, without credentials, but not its accounts. Create an IdentityCredential then fetches each registered provider’s accounts like any other IDP’s: the user agent checks its login status, fetches its 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.

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. If provider’s issuer metadata’s federations doesn’t exist or doesn’t contain federationRequest.federation, return failure.

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

  3. Return success.

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 result be the result of resolve a handle with handle and globalObject. If result is failure, return to the dialog.

    3. Let provider be the result of create a registered provider with result’s issuer, « handle », and federationRequest.

    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 create a registered provider from type your handle, rather than repeating them.

6.4. Issuing the HVT+KB

When the user selects an account of a registered provider, the RP receives an HVT+KB Handle Verification Protocol, rather than the token of Federated Credential Management API’s id_assertion_endpoint. The user agent obtains an HVT from the issuer with an HVT request, which doesn’t reveal the RP to the issuer, and binds it to the RP itself.

In fetch an identity assertion, insert the following step at the beginning:
  1. If provider is a registered provider, return the result of request an HVT+KB with provider, the account that the user selected, and globalObject, as the token.

To request an HVT+KB given a registered provider provider, an IdentityProviderAccount account, and a globalObject, run the following steps. This returns a string or failure.
  1. Let handle be the result of parse a handle with account’s handle. If handle is failure, return failure.

  2. Let result be the result of resolve a handle with handle and globalObject. The user agent MAY reuse the result of an earlier resolution of handle. If result is failure, or its issuer isn’t provider’s issuer, return failure.

  3. Let nonce be provider’s nonce. If it doesn’t exist, return failure.

  4. Generate a new key pair.

  5. Send an HVT request to provider’s issuer metadata’s issuance_endpoint, as defined by Handle Verification Protocol, with handle and result’s unique user identifier, signed with the key pair. The request includes the issuer’s cookies, and doesn’t include a referrer or reveal globalObject’s origin.

  6. If the response is an HVT error response, return failure.

    Note: The user agent sends an HVT request only for an account that it obtained from the issuer’s accounts_endpoint, which it fetches only when the user is logged-in to the issuer. An authentication_required error is therefore unexpected, and the user agent treats it, like any other error, as failure.

  7. Let hvt be the HVT in the response. If it fails HVT verification, including the checks that the browser performs, return failure.

  8. Return the HVT+KB formed from hvt and a KB-JWT, signed with the key pair, whose aud is the serialization of globalObject’s associated Document’s origin and whose nonce is nonce, as defined by Handle Verification Protocol.

Note: The RP verifies the HVT+KB as described in HVT+KB verification, including resolving the handle itself. The verification is the same whichever federation the RP asked for. The sub claim of the HVT carries the user’s unique user identifier, which the RP uses to identify the account.

7. Requirements on other parties

This section is non-normative.

Party What it does
IDP Is the authorized issuer of its users' handles, as defined by Handle Verification Protocol. Serves its issuer metadata at /.well-known/handle-verification, including the federations its accounts belong to. Serves a well-known file at its site that names its accounts_endpoint and login_url. Answers HVT requests at its issuance_endpoint. 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 Publishes the records that HVP resolution reads, which name the issuer: a DID and a DID document with a HandleVerification service.
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 a nonce, and verifies the returned HVT+KB as described in HVT+KB verification.

8. Privacy considerations

Request When Credentials Reveals the RP?
Operator metadata of the requested federation On a federation request, unless cached No No
HVP resolution of each registered handle On a federation request, unless cached No No
Issuer metadata and well-known file of registered issuers On a federation request, unless cached No No
accounts_endpoint of registered issuers On a federation request, when the IDP’s login status is logged-in Yes No
HVP resolution, issuer metadata, well-known file, and (when logged-in) accounts_endpoint of the resolved issuer 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
HVT request to the issuance_endpoint After the user selects an account Yes No

See also the privacy considerations of Handle Verification Protocol.

9. Security considerations

10. Examples

This section is non-normative.

A domain handle on a self-hosted server. 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 the domain handle alice.example.

  2. HVP resolution finds the DID that alice.example publishes, did:plc:..., whose DID document claims at://alice.example back and names https://pds.alice.example in its HandleVerification service.

  3. https://pds.alice.example/.well-known/handle-verification lists https://atproto.com in its federations, and https://alice.example/.well-known/web-identity names its accounts endpoint and login URL, so the user agent adds @alice.example to the handle registry.

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

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

  6. The user agent sends an HVT request for alice.example and did:plc:... to the issuance_endpoint, and returns the HVT+KB to the RP.

Many users share an issuer. Dave (@dave.social.example, a handle under the server’s domain) and Erin (@erin.example, her own domain), whose DID documents both name https://social.example as their issuer, both resolve to the same issuer. The handle registry has both handles, and group registered handles by issuer groups them together. On a federation request, the user agent fetches that issuer metadata and accounts_endpoint once, and shows the signed-in accounts that match @dave.social.example or @erin.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/
[HVP]
Sam Goto. Handle Verification Protocol. Internet-Draft. URL: https://samuelgoto.github.io/handle-verification/draft-goto-handle-verification.html
[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/
[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

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 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? ↵
Should other accounts from a registered IDP also be shown, once the user has registered at least one handle with it? ↵