Last reviewed: Jan '26
Introduction
You'll surely have noticed a growing trend for low-risk apps to use a packaged "Sign In with Google" (or similar mainstream identity provider) to sign in their users. This is an example of a federated FedCM (Federated Content Management) sign-in. In FedCM terms, Google is acting here as the Identity Provider (IdP), while the app is the Relying Party (RP). This post:
- Explains how "Sign in with Google" works
- Discusses why you might want to use it
- Provides a Svelte/Firebase example
- Describes the history of FedCM and "Sign in with Google"'s relationship with the new FedCM W3 standard
"Sign in with Google"
In the FedCM model, an IdP like Google lets you use a single account to sign in to multiple applications without creating new passwords.
Once "Sign in with Google" has verified that you are who you say you are (by virtue of your knowledge of an account's password and any associated 2FA credentials), it generates a cryptographically-secured token that the app can pass to secure backend processing. Here, the app extracts your account ID from the token and checks whether it is on its "whitelist" of permitted accounts.
The interaction is triggered by the display of a standard Sign in with Google button:
In a conventional sign-in, a webapp would use its own code to request and validate credentials. "Sign in with Google" enables you to replace this with Google library code. You also benefit from Google's secure cookies that allow Google to recognise returning users and issue fresh ID tokens. Your app can then sign in returning users with minimum fuss.
Here's an example of the dialogue (account id's have been blurred):
Here, you are explicitly told which app you're logging into and are assisted in selecting the Google account to use (you may have many - not necessarily all Gmail accounts). You are also given the opportunity to check which items (eg your name) may be supplied to the app from the information registered in your Google account.
Once signed in to a Google account, cookie caching can keep you signed in for a surprisingly long time. During this time, you may leave the browser, turn your computer off and back on again and still find yourself logged into the app.
But eventually, Google will decide that your "auth" is no longer valid and that it's time to re-authenticate.
Recognising that this creates significant "friction" for users, app developers can enable Google to let you sign back in with a One Tap prompt rather than the relatively cumbersome "Sign in with Google" procedure. The essence of this arrangement is that Google offers to skip the explicit account selection step of "Sign in with Google" by using the previously used account. The "One Tap" prompt appears as an ephemeral pop-up (formatted and displayed entirely by Google) that is floated alongside the conventional "Sign in with Google" button).
Here's a screenshot showing the interaction
As you can see, the ephemeral pop-up displays the account details for the targeted account (assuming it's still signed into Google) and invites you to re-enter the app by tapping the "Continue as ..." button. If you are unhappy that Google has selected the correct account, you can simply ignore the ephemeral and "Sign in with Google" instead.
As you may imagine, a good deal of Google magic is concealed within this procedure. All I'll say about it is that, in my experience, it seems to work well, but is difficult to test. This is because eligibility depends on prior user interaction, dismissal history, and browser heuristics that developers cannot directly control. More on this later.
Finally, I should say that in practice, you'll likely want to display your "Sign in with Google" button alongside those for other IdPs, such as Apple and Facebook. Here's an example of such an arrangement from the Binance site.
Note that this shows the use of "Continue with Google" as an alternative to "Sign in with Google" in the button text - see the sign-in-with-google code below for instructions on how to control both this and other characteristics of the button. Note also that the Binance login screen carries a button to register a Binance account for a user id. This is where Binance deploys the code to maintain its customer "whitelist".
Why might you use "Sign in with Google"?
For as long as I can recall, web users have been advised not to share passwords between apps. Now, Google is explicitly encouraging developers to write apps that authenticate using a common (Google) password.
If your Google account is ever compromised, all your associated "Sign in with Google" app activity is likewise at risk.
The first thing to remember, however, is that security arrangements for Google accounts are fearsome. I can guard my Google passwords with a variety of two-factor authentication methods, and Google will alert me to sign-ins from strange devices. It also provides recovery mechanisms to make it easy to reset a compromised password.
Developing a local sign-in to match such levels of sophistication would be quite a challenge.
Another advantage of using "Sign in with Google" is that it uses the FedCM (Federated Credential Management) API. This enables browsers to block third-party identity cookies while still permitting federated login. Google still uses cookies — but:
- They are first-party to google.com
- They are not readable by your site
- The browser mediates access
More on this in a moment.
If you like the look of "Sign in with Google" but are doubtful about the wisdom of using "One Tap", the code examples below show you how to suppress this.
On balance, I've decided to use "Sign in with Google" in my own apps. I doubt we'll see federated sign-ons used for the most critical applications, but in many other cases, they offer significant benefits.
How to code for "Sign in with Google"?
For your project to use "Sign in with Google", it first needs to register with the Google Cloud console, acquire a Google API client ID, and provide various details about itself.
(1) Get a Google API client ID for your project
If you are using Firebase, you will already have registered your project with the Google Cloud console and received a Google Project ID. But now you need an additional Google API Client ID.
The procedure is straightforward and well documented by Google at Get your Google API client ID.
Once you've got your Client Id, add it to your project's .env file. The entry should look something like:
VITE_GOOGLE_CLIENT_ID=108 .. obfuscated ... m2gjhau.apps.googleusercontent.com
(2) Configure an OAuth Consent Screen
The Google document also describes the procedure for filling out an OAuth Consent Screen that "tells users the application requesting access to their data, what kind of data they are asked for and the terms that apply". Importantly, it also tells Google which 'authorised domains' it may accept, i.e. the web addresses in your app that are permitted access. For testing my webapp, I only needed http://localhost:5173 (though obviously, you'd add others for live purposes later)
(3) Build yourself a "sign-in-with-google" page.
This is a page that will:
- load the "Sign in with Google" library
- configure the Google Identity service
- show the One Tap" ephemeral (if required)
- render a standard "Sign in with Google" button
Here's the code that I've been using. Since I'm a Firebase developer working on the Svelte platform, this may not map exactly to your case, but I hope you'll still find it useful.
routes/sign-in-with-google/+page.svelte
<script>
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { auth, signInWithGoogleIdToken } from "$lib/firebase.client.ts";
let returnTo;
// Load the Google Identity Services library
function loadGoogleScript() {
return new Promise((resolve) => {
if (window.google?.accounts?.id) {
resolve();
return;
}
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.async = true;
script.defer = true;
script.onload = resolve;
document.head.appendChild(script);
});
}
// Create a callback function to return control when Google successfully
// returns an ID token
function handleCredentialResponse(response) {
signInWithGoogleIdToken(response.credential)
.then(() => goto("/" + returnTo))
.catch((err) => console.error("Firebase sign-in failed", err));
}
onMount(async () => {
// Parse the redirectTo parameter from the current URL
const urlParams = new URLSearchParams(window.location.search);
returnTo = urlParams.get("returnTo") || "/";
// Load the GIS library i
await loadGoogleScript();
// Configures Google Identity Services (GIS). Nothing visible happens yet -
// - you have just set the rules.
google.accounts.id.initialize({
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
callback: handleCredentialResponse,
auto_select: false,
cancel_on_tap_outside: true,
});
// you have now:
// 1. Identified your app
// 2. Set a callback (handleCredentialResponse(credential)) that will fire
// when Google successfully authenticates the user.
// 3. Told Google not to sign the user in if Google already knows who they are.
// (Important for avoiding “why did it log me in?” surprises.)
// 4. Told Google that if the user clicks elsewhere, it should dismiss One Tap cleanly.
// At this point:
// - No sign-in has happened
// - No UI has appeared
// - You’ve just set the rules
// Tell Google to show the One Tap Sign-on if appropriate. Omit the following
// statement if you don't want this
google.accounts.id.prompt();
// Specify the format of the "Sign in with Google" button
google.accounts.id.renderButton(document.getElementById("googleBtn"), {
theme: "outline",
size: "large",
text: "signin_with", // or "continue_with"
});
// Skip page if already signed in
return auth.onAuthStateChanged((user) => {
if (user) goto(returnTo);
});
});
// This is completely independent of Google Identity Services. What happens here:
// 1. Firebase checks its persisted session (IndexedDB)
// 2. When resolved, it calls the callback once
// 3. If user exists: the user is already authenticated you immediately redirect to /returnTo
// 4. If user is null: stay on this page and let "One Tap" or "Sign In button do their work
//
// Key insight - you are coordinating three independent state machines:
// - Google Identity UI
// - Firebase authentication
// - SvelteKit navigation
//
// In summary: This code sets up Google sign-in UI, lets the user authenticate
// if needed, and uses Firebase as the single source of truth to redirect authenticated
// users without races, loops, or double sign-ins.
</script>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div
class="bg-teal-200 border border-black rounded-xl
px-[10vh] py-[10vh]
max-w-md w-full
flex justify-center"
>
<div class="rounded-md overflow-hidden">
<div id="googleBtn"></div>
</div>
</div>
</div>
TLDR: Notes
- The code above declares a function to load the Google Identity Services library. This is used to ensure the load completes successfully when the function is deployed later in the code. Google Docs say: "Be sure to load [the library] on any page that a user might sign in on". Naively, I thought this could be achieved by simply adding it to my project's app.html file. This didn't work at all! Svelte's complex hydration means you need to explicitly load it on your pages. The asynchronous code to load the library is called here with an
await. It is defined as a function in case circumstances change, and I need to centralise it as a "helper" - Confusingly, while Google Docs talk about the "Google Identity Services library", the library is actually held in "https://accounts.google.com/gsi/client"; GSI ("Google Sign In") is the historical API surface
(4) Create a Svelte fedcm-test route that triggers a login and displays some test data.
The following +page.svelte testbed triggers a sign-in to get a Google ID and, if successful, passes this to a +server.svelte file that checks if the user's account contained in the Google ID is included in the app's "whitelist" of permitted users. If the user is accepted, this server then returns a list of dataEntry items from a Firestore test_fedcm_data collection and +page.svelte displays them.
routes/fedcm-test/+page.svelte
<script>
import { onMount } from "svelte";
import { auth } from "$lib/firebase.client.ts";
let dataEntries = [];
let error = null;
let loading = true;
onMount(async () => {
try {
const user = auth.currentUser;
if (!user) {
throw new Error("Not authenticated");
}
// pass the user's idToken (containing their email address
// and other pieces of user information) to the secure
// server-based code in fedcm-test/+page.server
const idToken = await user.getIdToken();
const res = await fetch("/fedcm-test", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${idToken}`,
},
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error);
}
dataEntries = data.dataEntries;
} catch (err) {
error = err.message;
dataEntries = [];
} finally {
loading = false;
}
});
</script>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="flex flex-col items-center">
<!-- Teal panel -->
<div
class="
bg-teal-200 border border-black text-black
rounded-xl
w-[75vw] h-[75vh]
md:w-[50vw] md:h-[50vh]
flex
"
>
<div
class="
w-full h-full
overflow-y-auto overflow-x-hidden
overscroll-contain
pl-[15%] pr-4 py-4
"
>
{#if loading}
<p>Loading…</p>
{:else if error}
<p class="text-red-600">{error}</p>
{:else}
<ul class="space-y-2">
{#each dataEntries as dataEntry}
<li>{dataEntry}</li>
{/each}
</ul>
{/if}
</div>
</div>
</div>
</div>
(5) Create a server to check that the user is permitted to use the webapp and to return data for display
Here, in Svelte, I use a +server.js file. This means that all the processing here occurs securely on a cloud host (as opposed to insecurely using JavaScript on a web client). Because I'm using Google's Firebase, this requires me to obtain a Google service account.
routes/fedcm-test/+server.js
import { json } from "@sveltejs/kit";
import admin from "firebase-admin";
import serviceAccount from '/secrets/service-account-file.json';
// Retrieves the Google Account Id for the logged in user and check whether
// this appears in the "test_fedcm_whitelist" table. If so, it returns the
// contents of the test_fedcm_data table.
if (!admin.apps.length) {
// a firebase service account is required in order to authorise server-side
// firestore access. This is drawn from the project's "secrets" file
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
}
const db = admin.firestore();
export async function POST({ request }) {
try {
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return json({ error: "Missing auth token" }, { status: 401 });
}
const idToken = authHeader.split("Bearer ")[1];
const decoded = await admin.auth().verifyIdToken(idToken);
// idToken has been encrypted by "Sign in with Google" using Google's
// private keys. verifyIdToken() now validates it using Google's
// public keys and checks that it:
// . Was issued for your project (aud claim)
// . Comes from a trusted issuer (iss claim)
// . Hasn’t expired (exp claim)
const userEmail = decoded.email;
if (!userEmail) {
return json({ error: "No email on auth token" }, { status: 403 });
}
// Check that the user's Google Account Id is included in the list
// of authorised users
const fedcmWhitelistSnap = await db
.collection("test_fedcm_whitelist")
.where("whitelistEmail", "==", userEmail)
.limit(1)
.get();
if (fedcmWhitelistSnap.empty) {
return json({ error: "Access not allowed" }, { status: 403 });
}
// Fetch the list of fedcmData entries and return it as a JSON
const fedcmDataSnap = await db
.collection("test_fedcm_data")
.orderBy("dataEntry")
.get();
const dataEntries = fedcmDataSnap.docs.map(d => d.data().dataEntry);
return json({ dataEntries });
} catch (err) {
console.error(err);
return json({ error: "Server error" }, { status: 500 });
}
}
(6) Create some data in Firestore:
The code above assumes that you have also created two Firestore collections:
-
test_fedcm_whitelistcontaining a set ofwhitelistEmaildata items and -
test_fecm_datacontaining a set ofdataEntryitems
(6) Create a listener to ensure that all pages for this app trigger a login request before proceeding
In Svelte, I do this by creating a +layout.svelte file. By placing this at the top of the fedcm-test folder tree, I ensure that login will be enforced in all subordinate routes:
fedcm-test/+layout.svelte
<script>
import { onMount } from "svelte";
import { auth } from "$lib/firebase.client.ts";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
let user = null;
onMount(() => {
// set a listener that will be called whenever a page loads. If
// the user hasn't got a Google Auth (possibly because this has
// expired), they are directed to sign-in-with-google
return auth.onAuthStateChanged((u) => {
user = u;
if (!user) {
const returnTo = encodeURIComponent(
$page.url.pathname + $page.url.search,
);
goto(`/sign-in-with-google?returnTo=${returnTo}`);
}
});
});
</script>
{#if user}
<slot />
{/if}
(7) Test the code
When your app is running in a Svelte project's "dev" server, the browser should respond to a localhost:5173/fedcm-test URL by displaying a "Sign in with Google" button. Signing in here with a valid Google count that uses a Google account id that is included in the test_fedcm_whitelist collection should list the items included in the test_fcm_data collection.
If you rerun the localhost:5173/fedcm-test, you should find that "Sign in with Google" is skipped and the test_fedcm_data items are displayed directly - you are still signed into your Google account, and Google remembers its association with your app.
Google will break its association with the signed-in account if you wait long enough or use a "Sign out" button in your webapp. I haven't covered "Sign out" here, as this post is already way too long, but ChatGPT will surely give you some pointers. In such cases, you'll see the "Sign in with Google" button when you try to rerun the webapp.
For testing purposes, another way to force the "Sign in with Google" button to reappear is to run the app in an "incognito" window.
Display of the ephemeral will typically occur only after a considerable period of time, when Google decides it is necessary to confirm the webapp's association with a Google Account. It's not easy to trigger this for testing purposes, but I managed to generate the example shown earlier by creating a new Google profile and re-running the app.
Background on FedCM
Federated sign-on (or federated identity) emerged from enterprise and academic identity systems in the late 1990s/early 2000s. Instead of every system running its own usernames and passwords, the idea was to federate identities across organisational boundaries.FedCM is not a new auth model — it’s a browser-level evolution of these early ideas. It is Google (and others) saying
“Federated login is legitimate — let’s make it first-class and privacy-preserving.”
Development of the W3C FedCM standard has been ongoing since 2021 and has been controversial because it now aims to prevent IdPs such as Google and Facebook from silently tracking users across sites. Nevertheless, in 2024, Google Identity Services migrated “Sign in with Google” to FedCM by default in Chrome.
FedCM currently:
- Moves identity checking into the browser
- Removes the need for IdP third-party cookies in federated sign-in
- Prevents them from being used for cross-site tracking.
- Gives users explicit control and visibility
- Standardises the "sign in" experience in the browser
The specification is still evolving, but FedCM is now a living standard under the W3C.




Top comments (0)