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 belongs to 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. 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.
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:
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:
-
Defining the federation: which accounts belong to it, and what RPs can do with them beyond verifying that the user controls their handle. How RPs verify that is defined by Handle Verification Protocol, and is the same for every federation.
-
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. 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: 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. 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:
-
A handle is an HVP handle: a domain handle, such as
@alice.example, in its HVP normalized form. The user agent displays it with a leading@. A handle isn’t necessarily permanent: a user can change it, or move it to another IDP. -
An issuer is the IDP that is the authorized issuer of a handle, identified by its HVP issuer identifier, an
https:origin. -
To parse a handle given a string input, run the steps of HVP parsing on input, and return the resulting handle, or failure if they fail.
-
Two strings match as handles if parsing each of them succeeds and produces the same handle. This is the only comparison used on handles. It is applied to an account’s
handle, and never to itsusernameoremail. -
The issuer metadata of an issuer is its HVP issuer metadata, served at its HVP issuer identifier followed by
/.well-known/handle-verification. It lists the federations that the issuer’s accounts belong to in its federations member, and the endpoint that issues HVTs in its issuance_endpoint member. To fetch the issuer metadata of an issuer, the user agent fetches and checks it as defined by Handle Verification Protocol, without credentials, without a referrer, and from an opaque origin. -
Resolution runs HVP resolution on a handle, and produces a resolution result or failure. A resolution result has the following items:
- issuer
-
The HVP issuer identifier of the handle’s issuer.
- unique user identifier
-
The unique user identifier that the handle resolves to: a DID.
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.
-
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 records that HVP resolution reads: the HandleVerification service of the handle’s
DID document.
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 result be the result of resolve a handle with canonical and globalObject. If result is failure, queue to reject promise with "
NetworkError" and return. -
If globalObject is not on the issuer’s site of result, queue to reject promise with "
NotAllowedError" and return. -
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. -
If provider’s issuer metadata’s federations doesn’t exist or is empty, queue to reject promise with "
NetworkError" and return. -
Let accounts be the result of get the registered accounts with provider’s 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]. 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, 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 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 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 result be the result of resolve a handle with canonical and globalObject. If result is failure, queue to reject promise with "
NetworkError" and return. -
If globalObject is not on the issuer’s site of result, 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 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.
IdentityProviderAccounts accounts
and a handle handle, return the items of accounts whose
handle exists and matches handle.
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.
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 result be the result of resolve a handle with handle and globalObject. If result is failure, return failure.
-
Let provider be the result of create a registered provider with result’s issuer, « handle », null, and globalObject. If provider is failure, return failure.
-
If provider’s issuer metadata’s federations doesn’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, 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,configURLis 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.
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).
-
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 anaccounts_endpointand alogin_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.
https://social.example/.well-known/web-identity:
{ "accounts_endpoint" : "/accounts" , "login_url" : "/login" }
IdentityProviderAPIConfig or failure.
-
Let site be the registrable domain of issuer’s host. If site is null, return failure.
-
Let wellKnownUrl be a new URL whose scheme is "
https", host is site, and path is « ".well-known", "web-identity" ». -
Let wellKnown be the result of fetch the well-known file with wellKnownUrl and globalObject. If wellKnown is failure, return failure.
-
Let config be a new
IdentityProviderAPIConfig. -
For each member name m of « "
accounts_endpoint", "login_url" »:-
If wellKnown[m] doesn’t exist, return failure.
-
Let url be the result of running the URL parser on wellKnown[m], with wellKnownUrl as the base URL.
-
If url is failure, or its scheme is not "
https", or its origin is not same site with issuer, return failure. -
Set config[m] to the serialization of url.
-
-
Return config.
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.
configURL. In Federated Credential Management API:
-
In fetch the config file, if provider is a registered provider, return its config, without fetching anything.
-
Wherever an algorithm uses the origin of a provider’s
configURL, for example to get its login status, it uses the issuer of a registered provider instead. -
The
configURLof anIdentityCredentialcreated for a registered provider is the empty string. The RP identifies the issuer from theissclaim of the HVT.
IdentityProviderRequestOptions or null
federationRequest, and a globalObject, run the following steps. This returns an
IdentityProviderRequestOptions or failure.
-
Let metadata be the result of fetch the issuer metadata with issuer and globalObject. If metadata is failure, return failure.
-
Let config be the result of fetch the issuer config with issuer and globalObject. If config is failure, return failure.
-
Let provider be a new
IdentityProviderRequestOptions. -
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]. -
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.
-
Return provider.
IdentityProviderAPIConfig config, a
registered provider provider, and a globalObject, run the following steps. This returns a
list of IdentityProviderAccounts.
-
If get the login status for provider’s issuer 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 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.
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 issuer → handles of the result of group registered handles by issuer with globalObject:
-
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.
-
Let provider be the result of create a registered provider with issuer, handles, federationRequest, and globalObject. If provider is failure, continue.
-
If provider’s issuer metadata’s federations doesn’t exist or doesn’t contain federationRequest.
federation, continue. -
Append provider to result.
-
-
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.
-
If provider’s issuer metadata’s federations doesn’t exist or doesn’t contain federationRequest.
federation, return failure. -
Set provider’s accounts to the result of get the registered accounts with provider’s config, provider, and globalObject.
-
Return success.
-
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, displayed with a leading
@. -
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 issuer as 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 result be the result of resolve a handle with handle and globalObject. If result is failure, return to the dialog.
-
Let provider be the result of create a registered provider with result’s issuer, « handle », and federationRequest.
-
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 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.
-
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.
IdentityProviderAccount account, and a globalObject, run the following steps. This returns
a string or failure.
-
Let handle be the result of parse a handle with account’s
handle. If handle is failure, return failure. -
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.
-
Let nonce be provider’s
nonce. If it doesn’t exist, return failure. -
Generate a new key pair.
-
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.
-
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. Anauthentication_requirederror is therefore unexpected, and the user agent treats it, like any other error, as failure. -
Let hvt be the HVT in the response. If it fails HVT verification, including the checks that the browser performs, return failure.
-
Return the HVT+KB formed from hvt and a KB-JWT, signed with the key pair, whose
audis the serialization of globalObject’s associated Document’s origin and whosenonceis 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 |
-
Unlike Federated Credential Management API’s
id_assertion_endpoint, the HVT request doesn’t reveal the RP to the issuer: the user agent binds the HVT to the RP’s origin and nonce itself, in the KB-JWT, as described in Handle Verification Protocol. -
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 it to the DNS resolver and to the servers involved in its HVP resolution, 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 HVP resolution of the user’s registered handles, which the DNS resolver and the servers involved can observe. The requests don’t carry credentials or reveal the RP. User agents can reduce this by caching resolution results (see § 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.
See also the privacy considerations of Handle Verification Protocol.
9. Security considerations
-
Resolution isn’t proof, verification is. An attacker who manipulates the user agent’s resolution can make it 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 an HVT+KB for someone else’s handle, because the RP resolves the handle itself during HVT+KB verification. See the security considerations of Handle Verification Protocol.
-
Handles that are mistyped or don’t exist either fail to resolve, or resolve to a provider that has no matching account: 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, and the issuer doesn’t issue an HVT for a handle that the signed-in user doesn’t control.
-
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 the records that HVP resolution reads: if they change, 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 handle’s domain 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 caller to be on the issuer’s site of the handle’s resolution result, 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 the domain handlealice.example. -
HVP resolution finds the DID that
alice.examplepublishes,did:plc:..., whose DID document claimsat://alice.exampleback and nameshttps://pds.alice.examplein itsHandleVerificationservice. -
https://pds.alice.example/.well-known/handle-verificationlistshttps://atproto.comin its federations, andhttps://alice.example/.well-known/web-identitynames its accounts endpoint and login URL, 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=alice.example. Her server signs her in, and itsaccounts_endpointreturns an account with{handle: "@alice.example"}. -
The user agent sends an HVT request for
alice.exampleanddid:plc:...to the issuance_endpoint, and returns the HVT+KB to the RP.
@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.