CSP for Firebase Auth UI: Hosted UI vs Custom Flow

Table of Contents

Firebase Auth looks deceptively simple until you turn on a strict Content Security Policy.

That’s where things usually go sideways: popups stop working, redirect flows break, reCAPTCHA refuses to load, and suddenly your clean default-src 'self' policy turns into a pile of Google domains and exceptions. I’ve seen teams spend more time debugging CSP around auth than building auth itself.

If you’re using Firebase Auth UI, you’ve basically got two paths:

  1. Use Firebase-hosted auth flows and allow what they need
  2. Build a more custom auth UI and tighten CSP around your own app

Neither is perfect. The right answer depends on how much control you want, how strict your CSP needs to be, and how much pain you’re willing to absorb.

The real problem: Firebase Auth is not CSP-minimal

Firebase Auth UI pulls in a lot of moving parts:

  • Firebase SDKs
  • Google Identity endpoints
  • popup or redirect flows
  • iframes
  • reCAPTCHA for phone auth and abuse prevention
  • provider-specific assets

That means a strict CSP often needs allowances for:

  • script-src
  • connect-src
  • frame-src
  • style-src
  • sometimes img-src

If your goal is a locked-down CSP with nonces, no inline code, and minimal third-party origins, Firebase Auth UI will push back.

Option 1: Use Firebase Auth UI with a permissive, targeted CSP

This is the most common setup. You keep Firebase Auth UI, allow the required sources, and accept that your policy won’t be tiny.

A practical starting point looks something like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.gstatic.com https://www.googleapis.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://identitytoolkit.googleapis.com https://securetoken.googleapis.com https://www.googleapis.com;
  frame-src https://*.firebaseapp.com https://accounts.google.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

That’s not copy-paste complete for every provider, but it shows the shape of the policy.

Pros

Fastest to ship

If you need auth working this week, this is the path. Firebase Auth UI handles provider buttons, flows, and edge cases for you.

Less auth code to maintain

You’re not hand-rolling popup logic, redirect handling, account linking, or recovery flows. That matters.

Good enough security when done carefully

A CSP does not need to be tiny to be useful. A well-scoped allowlist is still far better than no CSP.

Cons

Your CSP gets noisy fast

Once you add Google Sign-In, Analytics, Tag Manager, consent tools, and whatever marketing added last quarter, your policy starts to look like a dependency graph exploded.

For example, a real production CSP from HeaderTest looks like this:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MjU4NThiYzItZTAwNC00NDQ1LTg2ODUtMzVlYWVkN2E5YThm' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

That header is solid, but it also shows reality: once third-party services pile up, CSP becomes a negotiation, not a purity contest.

Provider-specific debugging is annoying

Google, Apple, Microsoft, GitHub, phone auth — each one can add new endpoints or flow requirements. You’ll test one provider, ship, and another one breaks in Safari.

style-src 'unsafe-inline' often sneaks in

A lot of teams hate this, and for good reason. Firebase UI and surrounding frontend libraries can make strict style policies inconvenient.

Option 2: Build a custom auth UI with Firebase SDKs

This gives you more control over markup, scripts, and policy design. You still use Firebase Auth underneath, but you stop depending on the drop-in UI package.

A basic example:

import { initializeApp } from "firebase/app";
import {
  getAuth,
  GoogleAuthProvider,
  signInWithPopup
} from "firebase/auth";

const app = initializeApp({
  apiKey: "...",
  authDomain: "your-app.firebaseapp.com",
  projectId: "your-app"
});

const auth = getAuth(app);
const provider = new GoogleAuthProvider();

document.getElementById("login").addEventListener("click", async () => {
  try {
    const result = await signInWithPopup(auth, provider);
    console.log(result.user.email);
  } catch (err) {
    console.error(err);
  }
});

Your CSP can be tighter around your own code:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self' https://identitytoolkit.googleapis.com https://securetoken.googleapis.com;
  frame-src https://accounts.google.com https://your-app.firebaseapp.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

Pros

More control over CSP

You decide how scripts load, whether nonces are used, whether inline code exists, and how much third-party UI code gets pulled in.

Cleaner frontend architecture

I prefer this for modern apps. A custom login screen usually fits better with React, Vue, Svelte, or plain server-rendered pages than a drop-in widget from another era.

Easier to align with strict CSP patterns

If you’re aiming for nonce-based script-src with no inline JS and strong isolation, custom UI is the easier road.

Cons

You still don’t escape Firebase’s external dependencies

This is the part people miss. A custom UI does not mean default-src 'self' and done. Firebase Auth still talks to Google endpoints and may still need frames or popups.

More code, more responsibility

You own button behavior, loading states, provider errors, redirect recovery, and UX polish.

Phone auth gets ugly

If you use SMS auth, reCAPTCHA and related flows complicate CSP again. That’s usually where “we’ll just make it custom” stops being fun.

Option 3: Redirect-based auth instead of popup-based auth

This isn’t a full replacement for the two options above, but it’s a meaningful choice inside both.

await signInWithPopup(auth, provider);

Redirect flow

import { signInWithRedirect, getRedirectResult } from "firebase/auth";

await signInWithRedirect(auth, provider);

// later on page load
const result = await getRedirectResult(auth);
  • Better UX on desktop
  • Keeps user in context
  • Feels smoother in SPAs
  • More likely to hit browser restrictions
  • CSP and iframe interactions can be harder to debug
  • Popup blockers still exist because browsers love chaos

Redirect pros

  • Usually more reliable across restrictive browser setups
  • Simpler mental model for some identity providers

Redirect cons

  • Full-page navigation
  • Slightly clunkier UX
  • More state handling on return

If your CSP is already tight and popups are acting weird, redirect flow is worth trying before you start randomly adding domains to frame-src.

My opinionated take

If you want the lowest maintenance path, use Firebase Auth UI and write a CSP that explicitly allows only the sources you verify in production.

If you want the best long-term CSP hygiene, build a custom UI with Firebase SDKs and keep the auth surface small.

I usually avoid the drop-in Firebase UI for apps that already care deeply about CSP. Not because it’s bad, but because teams that care about CSP usually also care about controlling script loading, minimizing third-party code, and avoiding unsafe-inline. Those goals line up better with a custom auth layer.

Practical CSP tips for Firebase Auth UI

1. Start in report-only mode

Don’t guess. Ship a Content-Security-Policy-Report-Only header first and capture violations.

2. Separate auth requirements from marketing junk

A lot of bloated CSPs aren’t caused by auth at all. They’re caused by analytics, A/B testing, chat widgets, and consent tools. Keep those mentally separate.

3. Prefer specific directives over relaxing default-src

Don’t do this:

default-src 'self' https://*.google.com https://*.gstatic.com;

Do this instead:

default-src 'self';
script-src 'self' https://www.gstatic.com;
connect-src 'self' https://identitytoolkit.googleapis.com https://securetoken.googleapis.com;
frame-src https://accounts.google.com https://your-app.firebaseapp.com;

4. Use nonces where you control scripts

If your app server renders HTML, nonce your first-party scripts. The strict-dynamic pattern can work very well in modern apps, especially when you’re loading trusted bootstrap scripts.

5. Keep a known-good policy example library

When you need a starting point, csp-examples.com is useful for ready-to-use policy patterns. I still recommend validating every source against real network traffic before shipping.

Which approach should you choose?

Choose Firebase Auth UI if:

  • you want fast implementation
  • your team is fine with a somewhat broader CSP
  • you don’t want to own auth UX details
  • CSP is a security control, not a religion

Choose custom UI with Firebase SDKs if:

  • you want stricter CSP control
  • you already have a mature frontend stack
  • you want to avoid unnecessary third-party UI code
  • your team can handle auth edge cases without melting down

Choose redirect flow over popup if:

  • popup blockers or CSP issues are biting you
  • reliability matters more than smooth UX
  • your login flow is simple and infrequent

The biggest mistake is pretending Firebase Auth UI and an ultra-minimal CSP naturally fit together. They don’t. You can absolutely secure Firebase Auth well, but the cleanest result usually comes from accepting the tradeoff early: convenience now, or tighter control later.