A Work Item of the Federated Identity Community Group.
- Benjamin VanderSloot (Mozilla)
- Introduction
- Goals
- Non-goals
- Relying Party API, Getting a Credential
- Relying Party API, Finishing the Creation of a Credential
- Relying Party API, Using a Credential
- Identity Provider API, Allowing a Credential's Creation
- Key scenarios
- Detailed design discussion
- A light touch from the browser
- Using the Credential Manager
- Identity provider opt-in per relying party
- Scope of the credential's effectiveness and storage access
- Scope of the
crossSiteRequests
and lifetime of those requests - UI Considerations and identity provider origin
- Multiple identity providers
- The NASCAR problem
- 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 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 and after appropriate user mediation and identity provider opt-in an object is returned that gives the power to obtain unpartitioned cookies for the chosen identity provider.
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.
- Log in with Foo buttons
- Single-Sign On for domains that are not same-site
- Revisiting a page that has already been logged in with the API and presenting only the previously used identity provider
- Identity providers with bounce proxies
- Upgrade to FedCM in browsers that support it
It is an interesting possibility, but not yet done, to integrate solutions for:
- IDP discovery, reducing the need for NASCAR pages.
- Allowing account-specific details in the Credential to empower the UI to show that in the Credential Chooser dialog,
- Integrate with FedCM as a lightweight operating mode
- Custom identity provider infrastructure
- Generic prompts to "allow foo.com to track you"
- Per-identity Credentials
- Design of an identity protocol
The site that the user wants to log into needs to call the already existing method navigator.credentials.get()
. We put our arguments under a new key in the options argument, 'cross-site'
.
While not shown here, this can be combined with arbitrary other credential arguments.
let credential = await navigator.credentials.get({
'cross-site' : {
'allow-redirect' : true,
'providers' : [
{
"auth-origin": "https://login.idp.net",
"auth-link": "https://bounce.example.com/?u=https://login.idp.net/login.html",
},
]
}
});
This example shows the use perfect for a "Log in with Foo" button (use case #1, and use case #2), where one identity provider is presented, and if the user has not already logged in, they may be redirected to that provider's login page. This redirect behavior is only permitted when there is only one provider in the list. The 'allow-redirect'
field indicates that this is the expected mode. This is a separate redirect from the one present in the "auth-link"
field of the one provider, which is there to enable a bounce proxy for this identity provider (use case #4), so that the link can be to a different origin than the one that needs cookie access. If "auth-link"
is present, but "auth-origin"
is not, its value can be inferred as the origin of the link.
Another use example, provided below, shows how to request a credential from one of many IDPs the user may have already linked to this page (use case #3).
let credential = await navigator.credentials.get({
'cross-site' : {
'providers' : [
{
"auth-origin": "https://login.idp.net",
},
// ... many allowed ...
{
"auth-origin": "https://auth.example.biz",
},
]
}
});
Finally, while inconvenient, it would be possible for a site to dynamically choose between only one of FedCM and this API depending on FedCM's availability without further changes.
let opts = {};
try {
IdentityCredential
opts["identity"] = fedCMArgs;
} catch (e) {
opts["cross-site"] = crossSiteArgs;
}
let credential = await navigator.credentials.get(opts);
One odd hiccup we have is finishing the creation of a credential. While we can collect credentials from the credential store easily enough, our credential's discovery requires navigation away from the current document, leaving before the Promise returned from navigator.credentials.get
can resolve. Therefore, we have one extra API endpoint to facilitate the collection of those credentials that have just been discovered and lets us store them in the credential store, as shown below.
let credentials = navigator.credentials.crossSiteRequests.getAllowed();
for (let cred in credentials) {
navigator.credentials.store(cred);
}
With a Credential object in hand, a document can enable access to third-party unpartitioned cookies for a given origin with a single call. The credential must be same-site to the page which created it.
let credential = await navigator.credentials.get(opts);
await document.requestStorageAccess("cross-site" : credential);
The cookie access granted here should be identical to that of the Storage Access API, but provide the origin of the identity provider the credential corresponds to access to its cookies on the calling Document.
Credentials of this type are powerful, allowing third-party cookies to be sent for an origin that does not actively have a navigatable in the current navigatable tree. As such, we have to devise a new way for the identity provider to exhert control over which sites may create its credentials. There are two functions that enable this, both used in this code example:
for (let r in await navigator.credentials.crossSiteRequests.getPending()) {
if (IDP_DEFINED_ALLOW_SITE(r.origin)) {
navigator.credentials.crossSiteRequests.allow(r);
}
}
Here the identity provider chooses which sites may be valid relying parties dynamically from its own page, via the function IDP_DEFINED_ALLOW_SITE
, after enumerating all pending requests that exist for their use as an identity provider.
An identity provider may also provide either an allowlist of domains or an HTTP-endpoint that will reply with a success to a CORS requests from allowed relying parties to expidite future requets.
navigator.credentials.crossSiteRequests.allow({
"future-requests" : {
"allowlist": ["https://rp1.biz", "https://rp2.info"], // optional
"dynamic-via-cors": "https://api.login.idp.net/v1/foo", // optional
}
});
These APIs together enable login and linking scenarios that I have put into TODO categories.
In this case, our user has not used this identity provider (idp.net) on this site (example.com). They first interact with some UI in the page that is clearly associated with the identity provider and the following is called.
let credential = await navigator.credentials.get({
'cross-site' : {
'allow-redirect' : true,
'providers' : [
{
"auth-link" : "https://login.idp.net/login.html",
},
]
}
});
Browser UI is shown to the user that lets them pick to link their account to the identity provider. On selection, the browser redirects the navigatable to https://login.idp.net/login.html
.
There, the user may do some auth flow and on completion, the identity provider calls the following:
for (let r in await navigator.credentials.crossSiteRequests.getPending()) {
if (r.origin == AUTHORIZIONG_ORIGIN) {
navigator.credentials.crossSiteRequests.allow(r);
}
}
location.href = RETURN_TO_PAGE; // example.com page
This stores a new Credential in the Credential Store and enables a silent access for the site and navigates the user back. Upon return to the site to be logged into, the site runs the following:
let credentials = navigator.credentials.crossSiteRequests.getAllowed();
for (let credential in credentials) {
navigator.credentials.store(credential);
await document.requestStorageAccess("cross-site" : credential);
performLoggedInActions();
}
This can be run on every page load as it is guaranteed to provide no browser UI and provides the cross-site unpartitioned storage access desired.
As a prerequisite to this scenario, when the user logged into its identity provider, it called the following:
navigator.credentials.crossSiteRequests.allow({
"future-requests" : {
"dynamic-via-cors": "https://api.login.idp.net/v1/foo", // picking the more complicated option here.
}
});
As before, in this case, our user has not used this identity provider (idp.net) on this site (example.com). They first interact with some UI in the page that is clearly associated with the identity provider. The same code is called, and the same browser UI is shown. However, upon selecting to link with idp.net, the browser notices that it has a way to test if this is a valid origin. Since there is no allowlist, it sends a GET request to https://api.login.idp.net/v1/foo
with CORS header Origin: https://www.example.com
, and observes the response. If it is a successful response, the credential is returned. Then the site runs the following:
await document.requestStorageAccess("cross-site" : credential);
performLoggedInActions();
await navigator.credentials.store(credential);
In this scenario the user has made some indication to the site that they want to log in. The specifics of that interaction dictate what Credential types are appropriate. For sake of discussion, let's say the credentials defined here and a PasswordCredential would be good. The page then calls the following:
let credential = await navigator.credentials.get({
'password': true,
'cross-site' : {
'providers' : [
{
"auth-origin": "https://login.idp.net",
},
// ... many allowed ...
{
"auth-origin": "https://auth.example.biz",
},
]
}
});
Then the user is given any identity provider that they have already linked as an option in the browser UI, along with the password manager entries. Whichever is selected is returned. Note also that if only one Credential is in the store that is collected by this request, and "mediation" is "optional" or "silent", the browser will elide the UI, per the Credential Manager API.
Then the site can run the following:
await document.requestStorageAccess("cross-site" : credential);
performLoggedInActions();
await navigator.credentials.store(credential);
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 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.
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 navigator.credentials.crossSiteRequests
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 pending and allowed requests of the crossSiteRequests
interface is partitioned by top-level navigatable to preserve contextual integrity of the login flow. This means that popup flows are explicitly out of scope. We also dictate that the lifetime of a request should be at most an hour to prevent persistent tracking if a user backs out of an account linkage. Notably the pre-allowed identity providers are not partitioned by navigatable and are instead global.
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 punt on this for now, leaving it to the relying party to determine which IDP the user wants to link. However, given how simple the current structure of this proposal is and the benefit of solving this problem, we find this a compelling direction for exploration.
This was decided against because storing identity information in the browser from an identity provider was a hard constraint for the development of FedCM, and we wish to be able to store our credentials in the browser. We also find that this reduces the complexity of privacy analysis.
A previous attempt was made to integrate storable credentials into FedCM. This proved complicated and hard to do piecemeal and failed.
A middleground here may be to design this independently then add affordances to the Credential Manager API to hide one credential's interface type when another is present.
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.
The identity provider's use of navigator.credentials.crossSiteRequests
to allow future requests looks a lot like the Login Status API in FedCM. That would be a reasonable place to re-locate this function when the Login Status API sees multi-browser-adoption. However, for now, making future requests a variation on the allow()
call is simpler to explain and creates no external dependencies.
All names and strings are welcome to be bikeshed. Little care was put into picking the correct name for anything.
- Mozilla : Positive
Many thanks for valuable feedback and advice from:
- Tim Cappalli
- George Fletcher
- Sam Goto
- Yi Gu
- Johann Hoffmann
- Nicolas Peña Moreno
- Achim Schlosser
- Phil Smart
- Martin Thompson