Firebase Auth and social sign-in: the parts that are…
Social login is easy to add and easy to get subtly wrong. Custom claims that lie, rules that trust the client, and why an extension cannot use the Auth SDK…
Adding Google sign-in takes about twenty minutes. Getting the authorization behind it right takes considerably longer, and the gap between the two is where most of the real bugs live.
Authentication is not authorization, and the confusion is expensive
Authentication answers "who is this". Authorization answers "what may they do". Social sign-in solves the first completely and the second not at all — and because the first feels like the hard part, the second frequently gets whatever the client can be persuaded to enforce.
The concrete version: a signed-in user object with an isAdmin field on it, read from the profile document, checked in the UI. That is not a boundary. It is a rendering hint. The request still works if you send it yourself.
Where the role actually lives
The failure I keep meeting is a role the user can write. A profile document with an isAdmin field, and a rule permitting a user to update their own profile — which is reasonable, until you notice that "their own profile" includes the field that decides whether they are an administrator.
Two ways out, and the choice matters:
- Custom claims on the token. Set server-side only, arriving inside the ID token, so rules can read them without a document lookup. The trap is staleness — a claim changed on the server does not appear in a client's token until it refreshes, so a revoked admin stays an admin for up to an hour unless you force it.
- A role on a row the user cannot write. The column is outside the user's update grant, and every policy reads it through a helper. Slower by a lookup, and immediate — which for revocation is the property you want.
I use the second. I would rather pay a lookup than explain why someone I removed still had access after I removed them.
A rule must be able to prove itself from the query
This is the subtlety that produced twenty production defects across one project I worked on, with every static check green.
A rule like resource.data.ownerUid == request.auth.uid reads fine and is correct for a single-document read. On a list query it does not evaluate per document — the engine must be able to prove from the query's own filters that every document it could return satisfies the rule. A list with no where('ownerUid', '==', uid) is refused outright, and the failure surfaces as a permission error on a screen whose code looks perfect.
So: every list, count and aggregation carries the filter its rule depends on. Not for filtering — for provability.
Row-level security has the mirror-image trap
On the Postgres side the same idea has the opposite failure mode, and it is worse because it is silent. A policy filters. It does not refuse. So a read that should return nothing returns 200 and an empty array, and an update the user is not allowed to make returns 200 having changed zero rows.
Which means a probe that reports success proves nothing on its own:
- Seed data first. A 200 over an empty table passes vacuously — zero rows satisfy any rule.
- Send
Prefer: return=representation and read the row back. A bare 204 is not evidence.
- Test as a user who should be refused, not only as one who should succeed.
Extensions cannot use the Auth SDK at all
A browser extension is a separate case and it catches people out. Manifest V3 forbids remotely-hosted code, and the Firebase Auth web SDK loads code at runtime — so shipping it is a store rejection, not a warning.
The route that works is the browser's own identity API to obtain a token, and then verifying ownership on the data side through a field on the document rather than through the request's auth context. Everything else in the extension is bundled: no CDN scripts, no analytics that fetches its own library, no eval.
The small things that are actually the common ones
- Set the site URL and the redirect allowlist. A provider left at its factory default sends every completed sign-in to a dead address, and the fallback is silent rather than an error. I have found this configured wrong on a live project.
- Handle the cancel. A user closing the provider popup is a normal outcome, not an error. An error toast for a deliberate cancellation teaches people your app is broken.
- Decide account linking deliberately. Same email, two providers: either link them or refuse clearly. The default behaviour is rarely the one your product wants, and users who signed up with Google in March and press the other button in June will find whichever you chose.
- Never trust the email as an identifier. It is verified by the provider, it is not a primary key, and it can change.
Sessions, refresh, and the moment a signed-out user is not signed out
The part of authentication that gets the least attention is what happens after sign-in, and it produces the failures that are hardest to reproduce.
Tokens are short-lived and refresh in the background. That is mostly invisible, until a request goes out with a token that expired between being read and being sent, and the user sees a permission error on an action that should have worked. Retrying once on an authentication failure, after forcing a refresh, removes an entire category of intermittent bug that otherwise gets filed as "happens sometimes".
Sign-out is the one worth being careful about. Clearing the session is not enough on its own, because a client-side cache still holds everything the previous user was looking at. If the next person to use that browser signs in and the cache has not been cleared by key prefix, they are served the previous person's data from memory — not stale data, somebody else's account details rendered as their own.
So every query keyed to a user has its prefix listed in one place, and sign-out removes exactly those prefixes. The list is the kind that rots, which is why adding a user-scoped query means adding its prefix in the same change; a query whose prefix is missing survives the sign-out silently, and nothing about the resulting screen looks wrong.
The last one: decide what a signed-out visitor can see, deliberately. A large share of these products are useful before sign-in, and gating the whole application behind an auth check is both bad for discovery and dishonest to a crawler, which will index the sign-in page as though it were the content.
The test that finds all of this
Sign in as an ordinary user and try to do an administrator's job. Every layer that stops you is real; every layer that merely hides the button is decoration. It takes ten minutes and it is the only check in this entire post that cannot be faked by a green build.
https://aoneahsan.com/blog/firebase-auth-social-logins-security