A Work Item of the Federated Identity Community Group.
- Benjamin VanderSloot (Mozilla)
- Johann Hofmann (Google Chrome)
- Introduction
- Goals and Non-Goals
- Incremental Deployment of FedCM
- Considerations for Supplying IdP Configuration in setStatus
- Interaction with other FedCM Features
- Open Questions
- Detailed design discussion
- Considered alternatives
- Stakeholder Feedback / Opposition
- Acknowledgements
The goal of this project is to provide a purpose-built API for enabling secure and user-mediated access to cross-site top-level unpartitioned cookies.
This is accomplished with integration with the Credential Management API and the Login Status API to enable easy integration with alternative authentication mechanisms.
A site that wants a user to log in calls the navigator.credentials.get()
function with arguments defined in this spec. The browser ensures there is appropriate user mediation and identity provider opt-in. With those assurances, the browser may also decide there is no additional privacy loss associated with access to unpartitioned state, and choose to automatically grant access to Storage Access requests.
It is possible to look at this proposal as an “axiomatic base” of FedCM; simplifying FedCM down to its barest essentials that will allow it to still function in a way that provides value to both users and identity providers. One benefit of this approach is it allows for incremental adoption of full FedCM by existing identity providers. It defines useful user-agent behaviors for FedCM when some of the specified endpoints are not defined. This also has some implementation benefits for user-agent developers, as it does not require that an entirely parallel codepath be developed, tested, and maintained.
The following use cases are all motivating to this work and it is our goal to provide an easy-to-integrate solution for them that can be integrated into the Credential Manager as a unified browser-mediated login mechanism.
This feature is intended to:
- Allow IdPs to request Storage Access with a UI that gives users more context for their choices.
- Provide the benefits of the FedCM UX to IdPs whose system architecture or engineering budget does not permit adoption of full FedCM.
- Improve performance and privacy properties of FedCM by moving credentialed calls to IdPs to a later point in the sign-in user journey.
- Act as a supplement to FedCM, not an entirely parallel API surface; ideally, an understanding of FedCM should confer some understanding of Lightweight FedCM, and vice versa. This feature explicitly must /not/:
- Make invisible/silent timing attacks possible for a colluding IdP and RP.
- Create incentives for companies engaged in tracking to present themselves as a fake IdP. This feature is /not/ intended to:
- Entirely eliminate the need for some serving changes on the IdP; a limited number of new endpoints are acceptable in order to support some functionality.
- Create an entirely new type of credential or identity system.
If you are an IdP with existing infrastructure that relies on third-party cookie access to allow streamlined sign-in, ideally you will need to make minimal changes for your services to continue functioning in a world where a significant proportion of browsers no longer support unpartitioned third party cookie access. While you could have your embedded iframe on the RP call requestStorageAccess, that would require that the user interacts with the iframe. Additionally, the requestStorageAccess prompt in that case would not provide context about why you are requesting access; the user may well decline the request without understanding that it is necessary for sign-in to work smoothly.
// On the IdP's sign-in page after successful auth, or whenever a
// signed-in user visits the IdP.
// The profile information on accounts is a dictionary of the form defined in
// https://w3c-fedid.github.io/FedCM/#dictdef-identityprovideraccount
navigator.login.setStatus("logged-in", {
accounts: [{
id: "1234",
name: "John Doe",
email: "[email protected]",
picture: "https://example.com/users/foobar.jpg",
}],
expiration: 86_400_000 // 24 hours
});
// navigator.login.setStatus("logged-out"); undoes the prior operation
Even with just this client-side change, the browser will be able to give a meaningful prompt to the user when the following code is executed on the RP side.
try {
const {profile: {id, name, email, picture}} = await navigator.credentials.get({
identity: {providers: [{url: "https://example.com"}]}
});
// ... Post a message to the IdP iframe with the account ID
catch (e) {
// User either manually declined the prompt or the user was logged-out;
// importantly, these two outcomes are indistinguishable to the RP.
}
// Browser checks the login status for https://example.com; if there's a logged-in status with account information (that isn't expired), prompt the user with that account information. If not, issue a non-credentialled request to "https://example.com/.well-known/web-identity"; since this doesn't return the correct status+MIME type in this case, the browser is able to degrade gracefully: in passive mode, without a logged in account, the prompt is simply dismissed and an exception is thrown.
...
// In the IdP iframe, after receiving a postMessage from the toplevel document with the account ID:
await navigator.requestStorageAccess(); // Since the user selected an account already, this will be autogranted.
const token = (await fetch("https://example.com/login?rp=rp.example&id=" + id)).text();
// Do whatever you would usually do with your token here.
This approach works well with the Passive mode: if the user is not already signed in to the IdP, the browser doesn’t need to display anything. In Active Mode, the RP may wish to provide some guidance or trigger some action to get the user to refresh their IdP session, like navigating to the login page if it is known.
Note
We refer to a url
field on the RP-side provider configurations instead of configURL
; this is to align with future work deprecating the
configURL
parameter in favor of url
in FedCM.
For Active mode, if you want to be able to gracefully handle the signed-out case, IdPs have an incremental path: allow users to login using the “url” passed by the RP or (optionally) the IdP providing a custom one.
So, if an RP called:
let cred = await navigator.credentials.get({
identity: {
mode: "active",
providers: [{url: "https://example.com"}]
},
});
The browser would check the sign-in status list,
first by trying to resolve “https://example.com/.well-known/web-identity”. If the well-known file doesn’t exist (i.e., the request returns a status 404), the browser would use “login_url”
as the value of “https://” + eltd+1(“url”)
(i.e., in this example, “https://example.com”
). If an IdP requires a custom login_url
, they can have a very simple .well-known/identity
file, such as the following:
{
"login_url": "https://example.com/login"
}
Alternatively, the IdP can specify the login_url
as part of their navigator.login.setStatus
call:
navigator.login.setStatus("logged-in", {
accounts: [{
id: "1234",
name: "John Doe",
email: "[email protected]",
picture: "https://example.com/users/foobar.jpg",
}],
apiConfig: {
login_url: "/login.html";
}
expiration: 86_400_000 // 24 hours
});
On the other hand, if the user doesn’t have cached account info from the login status API (or if their cached info has expired), the user agent can present the user with the option to sign in with that IdP.
Most features of "Full" FedCM should still be available if the navigator.login.setStatus
implementation route is chosen.
Since the client_metadata_endpoint
is invoked with referrer details before the user has selected the IdP, an IdP that wished to track the user could store a decorated link that is unique to the user, and then join that unique ID with the request origin header. In order to prevent this, the client_metadata_endpoint
parameter should only be used when read from a configURL
supplied by an RP with the same .well-known/web-identity
constraints as defined in the full FedCM specification, not supplied via a setStatus
call. It would then be necessary for RPs to supply these details in the navigator.credentials.get
call. A set of parameters is proposed in a comment on the FedCM issue tracker..
const credential = await navigator.credentials.get({
identity: {
provider: [{
configURL: "https://idp.example.com/",
}],
rp: {
termsOfService: "https://rp.example/tos.html",
privacyPolicy: "https://rp.example/tos.html",
},
}
});
Unfortunately, not supporting the client_metadata_endpoint
removes the ability of IdPs to avoid reputational attacks that involve visually associating the IdP with an unapproved and undesired RP. IdPs that are sensitive to this kind of attack could supply a .well_known/web-identity
and config.json
and configure the client_metadata_endpoint
to return an error if the origin does not match their allow-list, as in "full" FedCM.
Since the accounts_endpoint
is invoked without referrer details, it shouldn't cause an IdP to become aware of the user's visit to an RP, even if the IdP is allowed to define it via setStatus
and not require a matching .well_known/web-identity
and JSON config URL. Since credentials are already being sent with the request, link decoration by the IdP would not provide them with any new information. Because the setStatus
is occurring on the IdP's origin ahead of time, there's no way to smuggle the RP's identity to the IdP before the user has linked their identity.
There are no special considerations for this, since this is only invoked after the user has confirmed that they want to link their identity between the RP and IdP.
Supplying this in n.l.setStatus
should be fine, since this will only be invoked after a user has linked their account in the first place.
Supplying this in n.l.setStatus
should be fine, with the caveat that the icon URLs provided must be retrieved when setStatus
is called, not when the RP calls n.c.get
.
The primary caveat is that if a login_url
is supplied in setStatus
, it could include identifying information for the user before the user has allowed linking between the RP and IdP. This can be partially alleviated by not supporting an RP-supplied loginHint
or domainHint
that would allow the IdP to create that linkage too early in the user journey for user permission to be gathered. The user agent can also choose to require additional confirmation before navigating to the loginUrl.
An IdP sending Login-Status: logged-in
refreshes the expiration timer on the stored profile information by resetting the browser's last-modified timestamp for that site's Login Status information.
Sending Login-Status: logged-out
clears the profile information along with the login status bit.
By default, FedCM makes a distinction between sign-up
and sign-in
via a property in the user profile information, called approved_clients
, that is received in the fetch the accounts step: if the clientId
passed in the navigator.credentials.get()
call is not a member of approved_clients
, it means that this client was never previously approved by the user.
This could also be accomplished client-side by passing a list of approved_clients
in the login status API call:
navigator.login.setStatus("logged-in", {
accounts: [{
// ... other fields
approved_clients: ["https://rp.example"],
// ... other fields
}]
});
An RP can also select which attributes of the user profile they would like the IdP or user agent to disclose. Currently, the following values are allowed: name
, email
, picture
, phoneNumber
, and empty list. This aligns with the current custom requests proposal.
This would also work client-side only, with the returned credential object only containing the specific attributes that were requested if they were stored in the setStatus
call.
const {profile: {email, name}} = await navigator.credentials.get({
identity: {
providers: [{
url: "https://example.com",
fields: ["email", "name"] // doesn't request the profile picture
}],
}
});
This would extend well if the user was "logged-in" in multiple IdPs, when an RP call could be as follows:
const {profile: {id, name, email, picture}} = await navigator.credentials.get({
identity: {
providers: [{
url: "https://idp1.example"
}, {
url: "https://idp2.example"
}]
}
});
In passive mode, an IdP can still use the IdP Registration API to expose itself for RPs that don’t want to enumerate IdPs:
// Prompts the user to accept this top-level origin as an IdP
await IdentityProvider.register("indie-auth");
Allows an RP to call:
await navigator.credentials.get({
identity: {
providers: [{type: "indie-auth"}]
}
});
- Should we allow defining the profile information and API config in the
Login-Status
header? - Does expiration also expire the IdP-supplied
apiConfig
or just the account information? - Does
Login-Status: logged-out
clear the IdP-suppliedapiConfig?
- Should the API config be supplied using a call to the IdP registration API instead?
- Given the lower barrier to entry and risk of abuse, should we support passive mode for Lightweight FedCM at all?
- Under what conditions is presenting IdP picker mandatory vs UA-implementation-defined?
- For
approved_clients
, should we support matching on the RP origin, not just the clientId?
Since any site can claim to be an identity provider with any "effectiveType"
, we may want to allow websites further control over the elements in the UI.
However this carries a risk of information leak to the relying party of all of the origins of a given type.
Currently the relying party may mitigate this by validating the origin of the returned credential, or by attempting to use the credential, and by repeating the authentication process if it is unacceptable.
Here is an example of such behavior in some abstracted Javascript:
while (true) {
let cred = navigator.credentials.get(options);
if (allowedOrigin(cred.origin) && credentialWorks(cred)) {
break;
}
}
useCredential(cred);
One core principal of this design is to get out of the identity provider's way as quickly and as much as possible. The purpose of UI when using this API should be to gather user consent to the linking of information between sites and then doing no more. Account selection, account data storage, policy presentation, and capability selection are all things we do not want to do as a browser as they are difficult and there is already an industry dedicated to solving these challenges. As such, each credential represents a connection to an identity provider, not an identity.
We chose to use the credential manager on the RP side here because we want this to be login-focused. It also provides a good deal of infrastructure in its design around mediation and allows us to potentially seamlessly integrate with all other login methods.
Using the Login Status API for the IdP side reflects that what is being stored is not, in and of itself, a credential. It is a declaration of the availability of a credential, with information to make it clear to the user what that credential is. It also definitively answers questions about how this functionality should behave if the user has been Login-Status: logged-out
by an IdP.
A natural question is: why can these credentials only be created via this weird dance that involves an identity provider page visit?
The answer lies in a constraint that the identity provider needs to pick and choose where it allows itself to use cross-site unpartitioned cookies carefully in order to mitigate CSRF attacks. So we have to allow the identity provider a say, and this is done via the IdentityCredential.requests
interface.
The credential provides cookie access to just the identity provider's origin. The security benefits of this are discussed elsewhere. We relax constraints on the relying party to site-scoping because login pages can reasonably be on different subdomains than the rest of the site. Because of the natural site-scoping of cookies, this has no privacy impact.
The credential chooser element for this credential and its discovery should show the identity provider's origin clearly so that the user can make a reasonable decision to link their informaiton between the identity provider and the site that they are on.
We permit the collection from several identity providers, however only one identity provider may be used when a redirect may occur. Because we do not have a good answer of how to solve the NASCAR problem, we don't want to re-create it in browser UI. So we only permit one IDP as an option when linking.
We make this a bit better by enabling discovery of a user-selected identity provider that has already been visited. The problem is not fully solved because users must visit the identity provider already to make use of this. Further improvements are welcome directions of future work.
Making this a distinct credential type from FedCM is a reasonable alternative, but was eventually decided against because of the semantics of this are so similar to that of an identity
Credential. It makes more sense to be a different operating mode of FedCM, with different arguments.
requestStorageAccessFor, top-level-storage-access, Forward Declared Storage Access, the old Storage Access API
Several proposals have been made to allow top-level storage access in a generic way. All of them are not use-case specific so their messaging to the user is not clear, making consent more difficult to gather. The flows of this API are nearly identical to that of top-level-storage-access, however this proposal gains all of the beneifts of integration with the credential manager.
All names and strings are welcome to be bikeshed. Ideally, names should be chosen to align as closely as possible with their equivalent concepts in "full" FedCM.
- Mozilla : Positive
Many thanks for valuable feedback and advice from:
- Tim Cappalli
- George Fletcher
- Sam Goto
- Yi Gu
- Nicolas Peña Moreno
- Achim Schlosser
- Phil Smart
- Martin Thompson
- Christian Biesinger
- Erica Kovac