FedCM for OAuth describes how OAuth clients and servers can use FedCM.
Note that the language used in FedCM is slightly different from the OAuth terminology. In particular:
- IdP - short for "Identity Provider", also called "authorization server"
- RP - short for "Relying Party", known in OAuth as the "client"
This guide assumes you are starting with a functional OAuth server, including authorization endpoint and token endpoint.
The first part of this will be setting up the core FedCM endpoints. You can follow the MDN IdP Integration guide for this.
Create a .well-known/web-identity
endpoint at your OAuth server's domain. This MUST be at the eTLD+1, and cannot be at a subdomain. For example, if your OAuth server is login.example.com
this file must go at example.com/.well-known/web-identity
. (See 580(https://github.com/fedidcg/FedCM/issues/580) for a possible DNS alternative.)
The well-known file must be served with the application/json
content type (See 589 for relaxing this). The content of the file is a JSON object with the full URL to the IdP's configURL.
https://example.com/.well-known/web-identity
{
"provider_urls": ["https://login.example.com/fedcm/config.json"]
}
The IdP config file contains links to other FedCM endpoints. This is a JSON document as well.
https://login.example.com/fedcm/config.json
{
"accounts_endpoint": "/accounts",
"client_metadata_endpoint": "/client_metadata",
"id_assertion_endpoint": "/assertion",
"login_url": "/login"
}
The properties are as follows:
accounts_endpoint
- The browser will make a request to this endpoint to determine if the user is logged in and fetch their photo/name to display in the widgetclient_metadata_endpoint
- This returns the client's terms of service and privacy policy URLs to the browser as registered at the IdP. See issue 581 for an alternative that lets the client provide these URLs instead.id_assertion_endpoint
- This is the endpoint the browser gets a credential from after the user clicks log in. For OAuth, this will be where the authorization server returns an authorization code to the browser.login_url
- If the user is not logged in, the browser will launch a popup window to this URL so the user can log in.
Note that the browser will not follow any redirects, these URLs in the config file must respond with JSON data directly.
The accounts endpoint is expected to return the list of accounts the user is logged in to at the IdP.
The browser will make a GET request to this endpoint along with any cookies for this domain, but does not contain a client_id
, Origin or Referer header.
GET /accounts HTTP/1.1
Host: login.example.com
Accept: application/json
Cookie: ebc7940893e0c4035e9179d0b83
Sec-Fetch-Dest: webidentity
The IdP checks the Sec-Fetch-Dest header and validates the cookie to determine which user is logged in, then returns the account information:
{
"accounts": [
{
"id": "1234",
"given_name": "Example",
"name": "Example User",
"email": "user@example.org",
"picture": "https://example.com/photo.jpg"
}
]
}
Note: The email
value will be displayed in the account chooser in the browser. Currently, this does not have to actually be an email address though (See 607 for possible enforcement), so you can return another user identifier like username or phone number instead . See issue 317 to track a proposal to allow free-form username
to be used.
If the user is not signed in, the IdP should return an HTTP 401
response.
The browser will request the client metadata from the IdP at this endpoint. This assumes this information has been registered at the OAuth server ahead of time.
{
"privacy_policy_url": "https://client.example.net/privacy_policy.html",
"terms_of_service_url": "https://client.example.net/terms_of_service.html"
}
This endpoint is what makes it all work. After the user clicks the browser "continue" button, the browser makes a request to this endpoint finally linking the RP and the IdP. The request is application/x-www-form-urlencoded
, and includes parameters with details of the attempted sign-in, as well as cookies from the IdP.
client_id
- The identifier of the clientaccount_id
- The ID of the account the user selected from the accounts endpointdisclosure_text_shown
- Whether the disclosure text was presented to the useris_auto_selected
- Whether the account was automatically selected (used in conjunction withmediation: optional
)params
- a JSON-encoded string of the custom parameters the client sent, including the PKCEcode_challenge
This request will contain cookies, so the IdP will know if the user is logged in and which user is logged in. Other validation steps the IdP does at this stage include:
- Ensure the
Sec-Fetch-Dest: webidentity
header is present - Validate the
client_id
is an acceptable client and that it's ok to process the authorization for this user for this client
For example, if the request contained scopes that the user has not previously authorized to this client, the IdP should not issue an authorization code at this time, and instead should get the RP to send the client through a normal redirect flow to obtain authorization.
At this point, the IdP can return data to the RP. The IdP builds a JSON string containing an OAuth authorization code. The client will later exchange this authorization code for an access token. The response should look like the below, including the CORS headers:
Content-type: application/json
Access-Control-Allow-Origin: https://client.example.org
Access-Control-Allow-Credentials: true
{
"token": "<authorization code>"
}
The fact that this authorization code is returned in a string called token
is a bit odd. See issue 578 to track progress on allowing the IdP to return JSON instead of a token
string.
Note: The Access-Control-Allow-Origin
header must not contain a trailing slash. If you don't set the right CORS headers here, the browser will throw an error in the console.
For any requests that include cookies, the browser will send a Sec-Fetch-Dest: webidentity
header as well. The IdP MUST validate the presence of this header to protect against CSRF attacks. This applies to the Accounts and ID Assertion endpoints.
The Login Status API allows an IdP to inform the browser of its login status. It is possible for the IdP to set this status either from JavaScript or an HTTP header. The IdP should update the status when the user logs in or logs out.
Setting the login status from an HTTP header:
Set-Login: logged-in
Set-Login: logged-out
Setting the login status from JavaScript:
navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");
If the IdP does not do this, the RP's call to FedCM will fail with the error "Not signed in with the identity provider" before the browser even attempts to fetch the accounts endpoint.
The IdP cookies must be set to SameSite=None
, HTTPOnly, and Secure. If these properties are not set, the browser will not send the cookies to the accounts or assertion endpoint, so it will always appear that the user is logged out.
The SameSite=None
requirement was added in Chrome 125. See issue 587 to track a proposal to change this to something else.
As an RP (client) using FedCM for OAuth, the process is as follows:
- Call
navigator.credentials.get
with aconfigURL
of the OAuth server you are trying to get an access token from - Upon receiving the authorization code (in the "token" property of the returned IdentityCredential), exchange the authorization code at the token endpoint
A detailed version of these steps is below.
First, from JavaScript, call navigator.credentials.get
with the desired configURL
. If the user is logged in, this will pop up the widget in the corner asking if they want to continue logging in to this site.
Until the RP can include arbitrary data to the assertion endpoint, you can use the nonce
parameter to include a PKCE code_challenge
. (This code_challenge
should be generated by your server and passed to the JS, ensuring that only your server ever has the code_verifier
value.)
const identityCredential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [
{
configURL: "https://login.example.com/fedcm.json",
clientId: "1234",
params: {
code_challenge: "<code_challenge>",
code_challenge_method: "S256",
},
},
]
},
}).catch(e => {
console.log("Error", e.message);
});
// If successful, identityCredential.token will be an OAuth authorization code
If the user clicks the "Continue" button, the browser will make a request with the IdP cookies to the ID assertion endpoint and return the response to your JavaScript code.
For OAuth, the response returned will be an authorization code. You'll need to exchange the authorization code for an access token at the token endpoint like in the traditional Authorization Code flow.
Typically an RP has a server-side component, so this also means your server can retrieve the tokens directly from the token endpoint.
So at this point, write some JavaScript to send this authorization code up to your server to continue the work.
if(identityCredential && identityCredential.token) {
const code = identityCredential.token;
const response = await fetch("/fedcm-login", {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
code: code
})
});
const responseData = await response.json();
// responseData will contain whatever your server responded with
}
Your server-side code will receive the authorization code from your JavaScript.
Make an OAuth access token request to the token endpoint including the authorization code and PKCE code_verifier
.
POST https://login.example.com/token
Content-type: application/x-www-form-urlencoded
Accept: application/json
grant_type=authorization_code
&code=<authorization code>
&client_id=1234
&code_verifier=a6128783714cfda1d388e2e98b6ae8221ac31aca31959e59512c59f5
The response will be the typical OAuth token response: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
The client can request OAuth scopes in the FedCM request using the params
object:
let {token} = await navigator.credentials.get({
identity: {
providers: [{
clientId: "1234",
configURL: "https://login.example.com/fedcm.json",
params: {
code_challenge: "<code_challenge>",
code_challenge_method: "S256",
scope: "photos:read photos:write",
}
},
}
});
This would be sent by the browser to the assertion endpoint as (PKCE parameters omitted for brevity):
POST /assertion HTTP/1.1
Host: login.example.com/
Origin: https://client.example.org/
Content-Type: application/x-www-form-urlencoded
Cookie: 0x23223
Sec-Fetch-Dest: webidentity
account_id=123&
client_id=client1234&
disclosure_text_shown=false&
scope=%7B%22photos:read+photos:write%22%7D
This request is effectively equivalent to an OAuth request with the OpenID Connect prompt=none
parameter, meaning there is no opportunity for the IdP to interact with the user before returning the successful response. So the IdP should only actually grant this request to the client if the user has already previously authorized this client with the requested scopes, following the same logic that would have applied to the IdP deciding to skip the consent screen on subsequent requests.
(TODO: Add notes for continuation API here.)
If the IdP does not want to issue the requested grant, it can:
- Return an authorization code for a grant without the full list of requested scopes, only the scopes previously authorized, which may be none
- Return a continuation API response
- Return an error response
In the case of returning an authorization code, the client will eventually find out that it wasn't granted the full list of scopes requested once it gets the access token response, at which point it can revert to a normal OAuth redirect flow to get the user's consent for the new scopes.
Alternatively to adding custom paramters in the FedCM request, the backend server could first do a PAR request including scope and any other parameters. See PAR below for details.
In the case that the IdP wants to require client authentication, such as when using the FAPI profile, the client will need to use a server-side backend to assist with the flow. This is already recommended in the OAuth for Browser-Based Apps BCP, which recommends using a backend as the OAuth client, keeping the actual access tokens out of the browser.
The way this use of FedCM is written, returning an authorization code from the FedCM response rather than an ID token, means there is a natural way to require client authentication already.
To better support more complex requests, OAuth clients may want to use Pushed Authorization Requests (RFC 9126).
Since this proposal already recommended the browser JS making an initial call to a backend server to request a PKCE code_challenge
, that step could also initiate a PAR request from the backend server as well. The response to the JS call would include the response from the PAR request, the request_uri
, which the browser would include in the FedCM call.
For example:
const startFlowResponse = await fetch("/fedcm-start", {
method: "POST"
});
const startFlowParams = await startFlowResponse.json();
// The code above tells the backend server of the client to initiate a PAR request
const parRequestURI = startFlowParams.request_uri;
// Then the PAR request_uri is included in the FedCM call, where the browser later sends it to the assertion endpoint
const identityCredential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [
{
configURL: "https://login.example.com/fedcm.json",
clientId: "1234",
params: {
request_uri: parRequestURI
}
},
]
},
}).catch(e => {
...
});
Adding OpenID Connect to this flow requires minimal changes. In fact, you could turn this into an OpenID Connect request by only adding the OpenID scope openid
to the FedCM request, and everything else stays the same.
let {token} = await navigator.credentials.get({
identity: {
providers: [{
clientId: "1234",
configURL: "https://login.example.com/fedcm.json",
params: {
scope: "openid",
}
},
}
});
In this case, the response would still be an authorization code, which the client backend would use at the token endpoint, getting an ID token in the response.
If you really wanted to, you could skip the authorization code step and use response_mode=id_token
to get the ID token in the FedCM response. The prompt=none
is implied in this request again, since there is no opportunity to insert user interaction in this flow.
let {token} = await navigator.credentials.get({
identity: {
providers: [{
clientId: "1234",
configURL: "https://login.example.com/fedcm.json",
params: {
scope: "openid",
response_mode: "id_token"
}
},
}
});
The token
variable in JS would be the OpenID Connect ID token.
The main downside to this approach is that in order to actually do anything with the ID token, you would need to do the full JWT validation wherever the contents of the ID token are consumed. For example, if the browser JS sent the ID token to a backend server, the backend server would need to do full JWT validation of the signature and claims, since the ID token would have come from the untrusted browser request.
In contrast, using the authorization code flow means the backend server receives the ID token in an HTTP response, so it can skip the JWT signature validation step.