A working example of Facebook Login and Google Login coexisting in a single app via the browser's Federated Credential Management (FedCM) API.
The example is wrapped in a small "Palette Collector" demo app — a color-palette generator with save / like / explore features — so the login flow has somewhere meaningful to land.
- Multi-provider FedCM — a single
navigator.credentials.get()call that lists both Facebook and Google inidentity.providers, so the browser shows one unified account chooser. - Mixing the FB JS SDK with native FedCM — the Facebook provider entry comes from
FB.FedCM.getProviderConfig(); the Google provider is configured directly in the call. The post-credential handoff differs per provider, and the sample shows both paths. - Server-side token verification — Google's JWT ID token is verified with
google-auth-library; Facebook's access token is exchanged at the Graph API. Both produce the same normalized profile shape. - Email-based account linking — when the same email appears under a different provider, the new provider is linked onto the existing user record. (Demo behavior — see Notes & Limitations.)
- Session management — once a token is verified, the server issues a standard
express-sessioncookie. No more FedCM round-trips per request.
- Chrome 136+ or Edge 136+ (multi-provider FedCM shipped April 2025). Firefox and Safari do not support FedCM.
- A Facebook App with Facebook Login configured and a Google Cloud project with an OAuth 2.0 Client ID. Add
https://localhost:3000as an allowed origin on both. - Node.js 18+.
- mkcert for local HTTPS — FedCM only runs in a secure context.
git clone https://github.com/fbsamples/fedcm-example-app.git
cd fedcm-example-app
npm install# macOS: brew install mkcert
# Linux: see https://github.com/FiloSottile/mkcert#installation
mkcert -install
mkdir -p certs
mkcert -key-file certs/localhost-key.pem -cert-file certs/localhost.pem localhostcp .env.example .envEdit .env:
| Variable | Required | Purpose |
|---|---|---|
FB_APP_ID |
yes | Your Facebook App ID. |
GOOGLE_CLIENT_ID |
yes | Your Google OAuth 2.0 Client ID. |
SESSION_SECRET |
yes | Any random string used to sign the session cookie. |
PORT |
no | Defaults to 3000. |
DEV_LOGIN |
no | Set to true to expose POST /auth/dev-login, a no-auth bypass that mints a fake session. Useful for poking at the palette features without a real IdP login. Never set in production. |
npm run start:httpsOpen https://localhost:3000 in Chrome 136+ and click Sign In.
To run tests:
npm testThe Facebook SDK loads asynchronously and the sample exposes its readiness as a Promise (window.fbSdkReady). Once both the SDK and FedCM are ready, the login function asks the FB SDK for its provider config, lists Google's provider directly, and fires a single navigator.credentials.get():
await window.fbSdkReady;
const { nonce } = await fetch("/auth/nonce").then((r) => r.json());
const fbProvider = FB.FedCM.getProviderConfig({
scope: "public_profile,email",
nonce,
});
const googleProvider = {
configURL: GOOGLE_CONFIG_URL,
clientId: GOOGLE_CLIENT_ID,
nonce,
params: {
nonce,
response_type: "id_token",
scope: "openid email profile",
},
};
const credential = await navigator.credentials.get({
identity: {
providers: [googleProvider, fbProvider],
},
});The browser shows a unified account chooser. The returned credential.configURL identifies which provider the user picked.
For Facebook, the credential is handed back to the SDK so it can finish the flow and yield a Graph API access token:
if (credential.configURL === FACEBOOK_CONFIG_URL) {
const result = FB.FedCM.processCredential(credential);
token = result.authResponse.accessToken;
} else {
token = credential.token; // Google: a JWT (or a JSON envelope wrapping one)
}The token is then POSTed to /auth/verify along with the configURL.
The server routes the token to the right verifier based on the configURL:
- Facebook —
GET https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=.... A 200 with anidfield counts as a verified token. - Google —
OAuth2Client.verifyIdToken({ idToken, audience: GOOGLE_CLIENT_ID })fromgoogle-auth-library, which checks the JWT signature against Google's published JWKS, validatesiss, and binds the audience to your Client ID. Google's FedCM response can be either a bare JWT or a{ iss, id_token, ... }envelope, so the sample unwraps the envelope before verifying.
Both verifiers run after a session-bound nonce check: the client calls GET /auth/nonce before each FedCM attempt, the server stashes the value in the session, and POST /auth/verify requires the IdP-returned token to carry that exact nonce. The session nonce is one-shot — consumed on every /auth/verify call so a captured token can't be replayed. Google's nonce is in the JWT nonce claim; Facebook's exchange returns an OAuth access token instead, so its replay protection comes from the Graph API audience binding rather than the nonce itself.
Both paths return a normalized profile:
{ provider, providerId, email, name, picture }findOrCreateUser(profile) walks three cases in order:
- A user already exists with this exact
(provider, providerId)— return it (and refreshname/picture). - A user already exists with this
email— link the new provider onto that account. - No match — create a new user.
The result is an in-memory record like:
{
id: 1,
email: "alice@example.com",
name: "Alice",
picture: "...",
providers: {
facebook: { id: "fb-123", email: "alice@example.com" },
google: { id: "goog-456", email: "alice@example.com" },
},
}Once findOrCreateUser returns, the server stashes the user's internal id in the session cookie (req.session.userId = user.id). Every subsequent request — palette CRUD, GET /auth/me, etc. — is a plain authenticated request gated on req.session.userId. FedCM is only involved at sign-in.
├── client/
│ ├── index.html Palette generator (login entry point)
│ ├── my-palettes.html Signed-in user's saved palettes
│ ├── explore.html Public feed of saved palettes
│ ├── auth.js FedCM client logic + FB SDK glue
│ ├── api.js fetch() wrappers for /api/palettes
│ ├── components.js Shared React components (Nav, PaletteCard, ...)
│ ├── colors.js Hex/HSL math, contrast, random color
│ ├── toast.js Toast notifications
│ ├── styles.css
│ └── favicon.svg
├── server/
│ ├── index.js Express app, routes, HTTPS bootstrap
│ ├── auth.js Token verification (Facebook + Google)
│ ├── users.js In-memory user store + account linking
│ └── palettes.js In-memory palette store
├── certs/ mkcert-generated local TLS certs (gitignored)
├── .env.example
├── package.json
├── LICENSE
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
The client uses React (loaded from a CDN) and @simonwep/pickr for the color picker. JSX is compiled in the browser via @babel/standalone to keep the demo build-step-free; a real production app should replace this with a bundler (Vite, esbuild, etc.).
| Browser | FedCM | Multi-provider |
|---|---|---|
| Chrome 136+ | Yes | Yes |
| Edge 136+ | Yes | Yes |
| Firefox | No | No |
| Safari | No | No |
On unsupported browsers, the sign-in surface shows a "FedCM not supported" notice. A production app should fall back to redirect-based OAuth — see Fallback Strategies.
- HTTPS required — FedCM is only enabled in secure contexts. Make sure you're running
npm run start:https, notnpm start. - Browser version — multi-provider needs Chrome 136+ / Edge 136+. Check
chrome://version. - Provider login state — the browser silently skips providers where you aren't signed in. Make sure you're logged into both
facebook.comandaccounts.google.comin the same browser profile. - DevTools console — FedCM surfaces detailed error messages in the Console tab.
- Mismatched client IDs — the App ID and Client ID in
.envmust match the Facebook App Dashboard and Google Cloud Console respectively. - Facebook App in development mode — your Facebook account must be listed as a developer, tester, or admin of the app, or the Graph API call will reject the token.
- Wrong audience — Google verification uses
audience: process.env.GOOGLE_CLIENT_ID. If you regenerated the Client ID, update.env.
The linking lookup is by exact email match. If your Facebook and Google accounts are registered under different emails, they'll be treated as two separate users.
For an app that needs to support all browsers:
- Feature-detect FedCM — check for
window.IdentityCredential(isFedCMSupported()inclient/auth.js). - Fall back to redirect-based OAuth — use the standard authorization-code flow for Facebook and Google. Both providers' SDKs handle this without FedCM.
- Reuse the same backend —
POST /auth/verifyworks for any token shape the providers return; only the client-side acquisition differs.
This is a sample. To keep the surface area small a few production concerns are deliberately out of scope:
- In-memory storage —
usersandpalettesare arrays in process memory. Restarting the server wipes everything. - Email-based account linking — linking by email assumes the email has been verified by the provider. For real systems, check
email_verifiedon Google's payload, treat Facebook email as untrusted-by-default, or replace the auto-link with an explicit "Link account" flow initiated by the already-signed-in user. - Session store —
express-session's default in-memory store is single-process and leaks; production apps should use Redis, Postgres, or another shared store. /auth/dev-login— gated only onDEV_LOGIN === "true". Treat that env var like a master key.
See CONTRIBUTING.md for how to send pull requests, file issues, and complete the CLA. Participation is governed by our Code of Conduct.
This project is licensed under the license found in the LICENSE file in the root directory of this source tree.