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 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. -
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 (find your account).
Both ways of registering a handle run the same resolution and add the same handle to the handle registry.
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:
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.
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 |
-
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.
-
If handle starts with "
https://", return the host of the result of running the URL parser on handle. -
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 | |
-
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 the config URL of its issuer.
-
Let issuer be the result of resolve the issuer of handle. If issuer is failure, return failure.
-
Let wellKnown be the result of fetch the well-known file with issuer and globalObject. If wellKnown is failure, return failure.
-
If wellKnown["
provider_urls"] doesn’t exist or its size is not 1, return failure. -
Let url be the result of running the URL parser on wellKnown["
provider_urls"][0]. -
If url is failure, or url’s scheme is not "
https", or url’s host’s registrable domain is not issuer, return failure. -
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.exampleis onalice.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.exampleis onsocial.example. Alice keeps her domain as her handle but uses a provider as its issuer, so she publishes a delegation record onalice.examplenamingsocial.example. - The provider assigned the handle under its own domain
-
The issuer of
@alice.social.exampleand ofalice@social.exampleis onsocial.example. This is really the first case: the handle’s own site issocial.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.
-
Let domain be handle’s domain.
-
Let records be the result of look up the delegation records for domain.
-
If records is failure or is empty, return domain’s registrable domain.
-
If records’s size is greater than 1, return failure.
-
Let issuer be the result of running the host parser on the ASCII lowercased records[0].
-
If issuer is failure or is not a site host, return failure.
-
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?
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.
-
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 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
setStatus() method so that it takes an optional
second argument, optional LoginStatusOptions options = {}, and add the following steps at the
end of its algorithm:
-
If status is
"logged-out", remove account registry[origin] and return. -
For each account of options["
accounts"]: if account has none ofname,email,tel,username, orhandle, throw aTypeError. -
Let expiration be null.
-
If options["
expiration"] exists, set expiration to the current time plus options["expiration"] milliseconds. -
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].
IdentityProviderAccounts.
-
If account registry[origin] does not exist, return an empty list.
-
Let entry be account registry[origin].
-
If entry’s expiration is not null and is in the past, remove account registry[origin] and return an empty list.
-
If get the login status for origin is not logged-in, return an empty list.
-
Return entry’s accounts.
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.
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 configUrl be the result of resolve a handle with canonical and globalObject. If configUrl is failure, queue to reject promise with "
NetworkError" and return. -
If configUrl’s origin is not same site with globalObject’s associated Document’s origin, queue to reject promise with "
NotAllowedError" and return. -
Let accounts be the result of find matching accounts with the result of get the pushed accounts for configUrl’s origin, and canonical.
-
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. -
Let provider be the result of create a registered provider with configUrl and null.
-
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. -
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 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.
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 configUrl be the result of resolve a handle with canonical and globalObject. If configUrl is failure, queue to reject promise with "
NetworkError" and return. -
If configUrl’s origin is not same site with globalObject’s associated Document’s origin, 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).
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
federationURLs that the IDP supports. An IDP that doesn’t list a federation is never returned for a federation request for that federation.
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.
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.
-
If federationRequest is not null:
-
Let registered be the result of gather registered providers with federationRequest, providerList, and globalObject.
-
Extend providerList with registered.
-
Then, in the step "For each provider in providerList", insert the following as the first sub-step:
-
If provider is a registered provider:
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:
-
If providerMap is empty:
-
If federationRequest is null, or mode is not
active, return (failure, false). -
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:
-
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:
-
Let account be the result of sign in with a handle with handle, provider, and globalObject.
-
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:
-
If federationRequest is not null, the user agent SHOULD add a find your account affordance. If the user triggers it:
-
Let result be the result of find your account with federationRequest and globalObject.
-
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.
-
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.
-
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)?
login_hint:
-
If provider is a registered provider with an associated handle handle, and provider’s
loginHintis empty, append ("login_hint", handle) to queryList.
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.1. Requesting by federation
partial dictionary IdentityProviderConfig {USVString federation ; };
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,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.
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.
- 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.
IdentityProviderRequestOptions or null federationRequest, run the following steps. This returns an
IdentityProviderRequestOptions.
-
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.
-
Return provider.
IdentityProviderAccounts.
-
Let pushed be the result of get the pushed accounts for configUrl’s origin.
-
Let result be an empty list.
-
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.
-
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?
-
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 once.
IdentityProviderRequestOptions
federationRequest, a list of IdentityProviderRequestOptions explicitProviders, and a
globalObject, run the following steps. This returns a list of registered providers.
-
Let federation be federationRequest.
federation. -
Let seenOrigins be the ordered set of the origins of the
configURLs of explicitProviders. -
Let result be an empty list.
-
For each configUrl → handles of the result of group registered handles by config URL with globalObject:
-
Let origin be configUrl’s origin.
-
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.
-
Let provider be the result of create a registered provider with configUrl and federationRequest.
-
Let config be the result of fetch the config file with provider and globalObject.
-
If config is failure, continue.
-
If config.
federationsdoesn’t exist or doesn’t contain federation, continue. -
Let accounts be the result of get the registered accounts with configUrl and handles.
-
For each account of accounts where account["
picture"] exists, fetch the account picture with account and globalObject. -
Set provider’s config to config, its handles to handles, and its accounts to accounts.
-
Append origin to seenOrigins, and append provider to result.
-
-
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:
-
Its primary label is handle.
-
It SHOULD show the federation branding for the
federationrequested. If there isn’t any, 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. The user agent MAY add the federation branding as a badge.
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]
IdentityProviderRequestOptions federationRequest and a
globalObject, run the following steps. This returns a pair (an IdentityProviderAccount, a
registered provider), 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 provider be the result of look up a typed handle with input, federationRequest, and globalObject.
-
If provider is failure, tell the user that no account provider was found for input, and go back to the first step.
-
Let handle be provider’s handles[0].
-
If provider’s accounts is not empty, show each of them. Otherwise, show a handle chip for handle and provider.
-
Wait for the user to select one of them, or to cancel. If the user cancels, return null.
-
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. -
If the user selected an account account, return (account, provider).
-
Let account be the result of sign in with a handle with handle, provider, and globalObject.
-
If account is failure, go back to the first step.
-
Return (account, provider).
IdentityProviderRequestOptions federationRequest, and a globalObject, run the following steps.
This returns a registered provider or failure.
-
Let handle be the result of parse a handle with input. If handle is failure, return failure.
-
Let configUrl be the result of resolve a handle with handle and globalObject. If configUrl is failure, return failure.
-
Let provider be the result of create a registered provider with configUrl and federationRequest.
-
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, its handles to « handle », and its accounts to the result of get the registered accounts with configUrl and « handle ».
-
Return provider.
IdentityProviderAccount or failure.
-
Set provider’s associated handle to handle.
-
Let result be the result of show an IDP login dialog with provider’s config, provider, and globalObject.
-
If result is failure, return failure.
-
Let matches be the result of get the registered accounts with the result of running the URL parser on provider’s
configURL, and « 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.
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 |
-
Registered IDPs never get a credentialed request before the user picks an account or chooses to sign in. This removes the timing attack that affects the
accounts_endpoint, and the need for mismatch UI. -
Typing a handle reveals it to the DNS resolver and the resolved IDP’s servers, but not 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 domains 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 and account registry are global. They are used only in user agent UI, and are 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. Resolve the issuer falls back to the registrable domain, just as cookies and the FedCM well-known file do. 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 domain: if the domain’s delegation record or 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 domain later (for example, after it 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 types
@alice.examplein find your account. Parse a handle gives@alice.example, whose domain isalice.example. -
There’s no delegation record, so resolve the issuer returns
alice.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. -
No account has been pushed yet, so the user agent shows a handle chip for
@alice.example. -
When Alice selects it, the user agent adds
@alice.exampleto the handle registry and openslogin_url?login_hint=%40alice.example. Her server pushes{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 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.
@carol.social.example.
-
There’s no record at
_web-identity.carol.social.example, so resolve the issuer returnssocial.example, the registrable domain. -
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"). Her pushed account is cleared, but@erin.social.examplestays in the handle registry. On the next federation request, the user agent shows a handle chip for@erin.social.example. Selecting it openssocial.example’slogin_urlwithlogin_hint=%40erin.social.example. -
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.