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:
-
After the user signs in, an IDP registers the user’s handle by calling
register(), with the user’s permission. -
An RP requests any registered IDP that supports a federation it accepts, by passing a
federationinstead of aconfigURL: -
A user whose handle isn’t registered yet can type it, for example
@alice.example,alice@social.exampleorhttps://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.
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:
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:
-
Defining the federation: the protocol or profile that its IDPs and RPs follow, including how RPs verify the tokens that IDPs issue.
-
Publishing operator metadata about the federation, such as its branding, which the user agent uses in its UI (§ 6 Presentation).
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: itsname,icons,background_color, andcolor.
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" } }
IdentityFederationOperatorMetadata or failure.
-
Let federationUrl be the result of running the URL parser on federation. If federationUrl is failure, or its scheme is not "
https", return failure. -
Let url be a new URL with scheme "
https", host federationUrl’s host, port federationUrl’s port, and path « ".well-known", "web-identity-federation" ». -
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, andIdentityFederationOperatorMetadatain place ofIdentityProviderAPIConfig. 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
federation
federation and a globalObject, run the following steps. This returns an
IdentityProviderBranding or null.
-
Let metadata be the result of fetch the operator metadata with federation and globalObject.
-
If metadata is failure, or metadata["
branding"] doesn’t exist, return null. -
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.
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 |
-
Let s be input with leading and trailing ASCII whitespace stripped.
-
If s starts with "
https://":-
Let url be the result of running the URL parser on s.
-
If url is failure, return failure.
-
Let host be url’s host.
-
Let handle be the result of running the URL serializer on url.
-
-
Otherwise:
-
If s starts with "
@", remove the first code point from s. -
If s contains "
@":-
Let local be the part of s before the last "
@", and hostString the part after it. -
If local is the empty string, return failure.
-
-
Otherwise, let local be null and hostString be s.
-
Let host be the result of running the host parser on hostString.
-
If host is failure, return failure.
-
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.
-
-
If host is not a domain, return failure.
-
If host’s registrable domain is null, return failure.
-
Return handle.
-
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. -
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 | |
-
If a and b both start with "
https://":-
Let urlA and urlB be the results of running the URL parser on a and b.
-
If either is failure, return false.
-
Return whether urlA equals urlB.
-
-
Let |a'| and |b'| be the ASCII lowercased a and b.
-
If |a'| starts with "
@", remove its first code point. Do the same for |b'|. -
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.
-
Let issuer be the result of resolve the issuer of handle. If issuer is failure, return failure.
-
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.
-
Let records be the result of look up the delegation records for handle’s site.
-
If records is failure or its size is not 1, return failure.
-
Let value be records[0] with the leading "
iss=" removed. -
Let issuer be the result of running the host parser on the ASCII lowercased value.
-
If issuer is failure or is not a site host, return failure.
-
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?
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.
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).
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.
-
If the handle registry contains a handle that matches handle, return.
-
Append handle to the handle registry.
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.
register(handle) method is invoked, run
the following steps:
-
Let globalObject be the current global object.
-
Let promise be a new
Promisein globalObject’s relevant realm. -
If globalObject’s navigable is not a top-level traversable, reject promise with a "
NotAllowedError"DOMExceptionand return promise. -
In parallel, run the following steps:
-
Let canonical be the result of parse a handle with handle. If canonical is failure, queue to reject promise with "
SyntaxError" and return. -
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. -
If globalObject is not on the issuer’s site of metadata, queue to reject promise with "
NotAllowedError" and return. -
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. -
Let config be the result of fetch the config file with provider and globalObject.
-
If config is failure, or config.
federationsdoesn’t exist or is empty, queue to reject promise with "NetworkError" and return. -
Let accounts be the result of get the registered accounts with config, provider, and globalObject.
-
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_endpointfails, or if none of the returned accounts has canonical as itshandle. -
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. -
Add to the handle registry with canonical.
-
Queue a global task on the DOM manipulation task source given globalObject to resolve promise.
-
-
Return promise.
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.
-
The prompt SHOULD show handle as the account’s identifier.
-
The prompt SHOULD show config’s
branding, for example as a badge on the account’s picture. -
The user agent MAY require transient activation before showing the prompt, MAY rate-limit it, and MAY remember that the user declined and not ask again.
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.
unregister(handle) method is invoked,
run the following steps:
-
Let globalObject be the current global object.
-
Let promise be a new
Promisein globalObject’s relevant realm. -
In parallel, run the following steps:
-
Let canonical be the result of parse a handle with handle. If canonical is failure, queue to reject promise with "
SyntaxError" and return. -
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. -
If globalObject is not on the issuer’s site of metadata, queue to reject promise with "
NotAllowedError" and return. -
Remove from the handle registry with canonical.
-
Queue a global task on the DOM manipulation task source given globalObject to resolve promise.
-
-
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.
IdentityProviderAccounts accounts
and a handle handle, return the items of accounts whose
handle exists and matches handle.
4.3. Declaring supported federations
partial dictionary IdentityProviderAPIConfig {sequence <USVString >federations ; };
federations, of type sequence<USVString>-
The
federationURLs that the IDP supports. An IDP that doesn’t list a federation is never returned for a federation request for that federation.
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.
federation URLs
federations and a globalObject, run the following steps. This returns a handle or null.
-
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.
-
Wait for the user to submit a string input, or to cancel. If the user cancels, return null.
-
Let handle be the result of check a typed handle with input, federations, and globalObject.
-
If handle is failure, tell the user that no account provider was found for input, and go back to the first step.
-
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. -
Return handle.
federation URLs federations, and a globalObject, run the following
steps. This returns a handle or failure.
-
Let handle be the result of parse a handle with input. If handle is failure, return failure.
-
Let metadata be the result of resolve a handle with handle and globalObject. If metadata is failure, return failure.
-
Let provider be the result of create a registered provider with metadata, « handle », and null. If provider is failure, return failure.
-
Let config be the result of fetch the config file with provider and globalObject. If config is failure, return failure.
-
If config.
federationsdoesn’t exist, or none of its items is contained in federations, return failure. -
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,configURLis ignored, and the entry stands for every registered IDP whose config file lists this federation.
An IdentityProviderRequestOptions whose federation exists is a
federation request.
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.
mediation":
-
Let federationRequests be the items of providerList that are federation requests.
-
If federationRequests’s size is greater than 1, return (failure, true).
-
Let federationRequest be federationRequests[0] if it exists, and null otherwise.
-
Let federationBranding be null.
-
If federationRequest is not null:
-
Let registered be the result of gather registered providers with federationRequest, providerList, and globalObject.
-
Extend providerList with registered.
-
If providerList’s size is greater than 1, set the offered as a choice flag of each item of registered.
-
Set federationBranding to the result of get the federation branding with federationRequest.
federationand 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).
-
clientId) in the "Create a list" step to:
-
("client_id", clientId), where clientId is provider’s
clientIdif 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
- 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.
IdentityProviderRequestOptions or null
federationRequest, run the following steps. This returns an IdentityProviderRequestOptions or
failure.
-
If metadata["
provider_urls"] doesn’t exist or its size is not 1, return failure. -
Let configUrl be the result of running the URL parser on metadata["
provider_urls"][0]. -
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. -
Let provider be a new
IdentityProviderRequestOptions. -
Set provider.
configURLto the serialization of configUrl. -
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]. -
Mark provider as a registered provider, and set its handles to handles.
-
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)?
IdentityProviderAPIConfig config, a
registered provider provider, and a globalObject, run the following steps. This returns a
list of IdentityProviderAccounts.
-
Let origin be the origin of the result of running the URL parser on provider’s
configURL. -
If get the login status for origin is not logged-in, return an empty list.
-
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).
-
If provider is a registered provider, remove from accountList every account whose
handledoesn’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?
-
Let groups be an empty ordered map.
-
For each handle of the handle registry:
-
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.
IdentityProviderRequestOptions
federationRequest, a list of IdentityProviderRequestOptions explicitProviders, and a
globalObject, run the following steps. This returns a list of registered providers.
-
Let seenOrigins be the ordered set of the origins of the
configURLs of explicitProviders. -
Let result be an empty list.
-
For each (metadata, handles) of the values of the result of group registered handles by issuer with globalObject:
-
Let provider be the result of create a registered provider with metadata, handles, and federationRequest. If provider is failure, continue.
-
Let origin be the origin of the result of running the URL parser on provider.
configURL. -
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.
-
Append origin to seenOrigins, and append provider to result.
-
-
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.
-
Let config be the result of fetch the config file with provider and globalObject.
-
If config is failure, return failure.
-
If config.
federationsdoesn’t exist or doesn’t contain federationRequest.federation, return failure. -
Set provider’s config to config, and its accounts to the result of get the registered accounts with config, provider, and globalObject.
-
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.
-
If provider is a registered provider, and config.
federationsdoesn’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.
-
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:
-
Its primary label is handle.
-
If federationBranding is not null, it SHOULD show it, as described in § 2.2 Branding: for example, the federation’s icon, and its
nameas secondary text. Otherwise, it uses a generic icon. -
It MAY show the host of provider’s
configURLas secondary text, so that the user knows where they’ll sign in.
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.
IdentityProviderAccount or failure.
-
Let loginProvider be a copy of provider. If loginProvider’s
loginHintis empty, set it to handle.Note: Show an IDP login dialog sends loginProvider’s
loginHintto the IDP aslogin_hint. Only the login dialog uses loginProvider, so that the handle doesn’t also filter the accounts that are fetched afterwards. -
Let result be the result of show an IDP login dialog with provider’s config, loginProvider, and globalObject.
-
If result is failure, return failure.
-
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.
-
If matches’s size is 1, return matches[0].
-
If matches’s size is greater than 1, let the user choose one of matches and return it. If the user cancels, return failure.
-
Tell the user that the account they signed in with doesn’t match handle, and return failure.
Change the step "If providerMap is empty, return (failure, false)" to:
-
If providerMap is empty:
-
If federationRequest is null, or mode is not
active, return (failure, false). -
Otherwise, the user agent MAY continue, only allowing users to type your handle.
-
After the "Build UI" step, add the following step:
-
If federationRequest is not null, the user agent SHOULD allow users to type your handle. If the user chooses to:
-
Let handle be the result of type your handle with « federationRequest.
federation» and globalObject. If handle is null, return to the dialog. -
Let metadata be the result of resolve a handle with handle and globalObject. If metadata is failure, return to the dialog.
-
Let provider be the result of create a registered provider with metadata, « handle », and federationRequest. If provider is failure, return to the dialog.
-
If load a registered provider with provider, federationRequest, and globalObject returns failure, return to the dialog.
-
If provider’s accounts is not empty, show each of them. Otherwise, show a handle chip for handle, provider, and federationBranding.
-
Wait for the user to select one of them, or to cancel. If the user cancels, return to the dialog.
-
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.
-
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 |
-
A registered IDP whose login status is logged-out receives no credentialed request before the user chooses to sign in to it. When the IDP’s login status is logged-in, the user agent fetches its
accounts_endpointas in Federated Credential Management API (unless the IDP and user agent use Account Registry API, which avoids the credentialed fetch). -
Typing a handle reveals its site to the DNS resolver and to the resolved IDP’s servers, and signing in with a handle chip sends the handle to the IDP, but nothing is revealed to the RP until the user finishes signing in.
-
Because the handle registry stores only handles, a federation request can cause DNS lookups for the sites of the user’s registered handles, which the DNS resolver can observe. The requests don’t carry credentials or reveal the RP. User agents can reduce this by caching resolution results (see § 3.3 The Handle Registry).
-
The handle registry is global. It is used only in user agent UI, and is never exposed to the RP until the user selects an account.
-
The delay before rejection defined in Federated Credential Management API still applies, so an RP can’t tell whether the user has any registered IDPs.
9. Security considerations
-
Resolution isn’t proof. An attacker who spoofs DNS, or controls a site’s well-known file, can make resolution end at an IDP of their choosing. That gets them the typed handle. They don’t get the user’s credentials for the real IDP, and they can’t make the RP accept a token for someone else’s handle, as long as the RP verifies the token using its protocol (for example, atproto’s two-way check between handle and DID). This is the same reasoning as the Email Verification Protocol’s analysis of DNS delegation.
-
A site speaks for its subdomains. A handle’s site is its registrable domain, just as for cookies and the FedCM well-known file. Hosts that give independent users their own subdomains need to be on the Public Suffix List to keep them separate.
-
Handles that are mistyped or don’t exist still resolve to the provider. However, find matching accounts returns nothing, and the handle chip shows the handle exactly as typed.
-
An account that doesn’t match isn’t used: sign in with a handle fails if the account the user signs in with doesn’t match the handle.
-
Registration prompts can be abused for spam. User agents can rate-limit
register(), require transient activation, and remember when the user declines. -
**A handle’s issuer can change.** Because the handle registry stores only handles, a registered handle follows its site: if the site’s delegation record or the issuer’s well-known file changes, the handle is offered with the new issuer. That is how a user moves a handle to another IDP, but it also means that whoever controls the site later (for example, after the domain expires) controls the handle’s issuer. A user agent that caches resolution results MAY notice such a change and ask the user to confirm it.
-
**An IDP can only register handles it is the issuer of.**
register()requires the resolved config file to be same site with the caller, so a site can’t register handles whose issuer is another IDP. Similarly,unregister()only removes handles that resolve to the caller’s site.
10. Examples
This section is non-normative.
pds.alice.example, and her handle is @alice.example.
-
Alice chooses to type your handle and types
@alice.example. Parse a handle gives@alice.example, whose site isalice.example. -
_web-identity.alice.examplehasTXT "iss=alice.example", so resolve the issuer returnsalice.example. -
https://alice.example/.well-known/web-identityreturns{"provider_urls": ["https://pds.alice.example/config.json"]}, which is on the same site. -
The config file lists
https://atproto.cominfederations, so the user agent adds@alice.exampleto the handle registry. -
Alice isn’t signed in to
pds.alice.exampleyet, so the user agent shows a handle chip for@alice.example. -
When Alice selects it, the user agent opens
login_url?login_hint=%40alice.example. Her server signs her in, itsaccounts_endpointreturns an account with{handle: "@alice.example"}, and the user agent requests the token.
@bob.example, and his
account is hosted by social.example.
-
Bob publishes
_web-identity.bob.example TXT "iss=social.example". -
Resolve the issuer returns
social.example. The user agent fetcheshttps://social.example/.well-known/web-identityand then its config file. -
The user agent shows Bob’s account with
handle: "@bob.example"if he is signed in tosocial.example, and otherwise offerssocial.exampleas 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.
@carol.social.example.
-
Its site is
social.example, which publishes_web-identity.social.example TXT "iss=social.example", so resolve the issuer returnssocial.example. -
Resolution continues as in the previous example.
@erin.social.example from
social.example.
-
Erin signs out of
social.example, which callsnavigator.login.setStatus("logged-out").@erin.social.examplestays in the handle registry. On the next federation request, the user agent sees thatsocial.exampleis logged-out and offers it as Federated Credential Management API does for an IDP the user is logged-out of. Signing in openssocial.example’slogin_url. -
Later, Erin deletes her account.
social.examplecallsIdentityProvider.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.