Files
server-26/infra/ansible/roles/deploy/templates/Caddyfile.j2
T
Logan CusanoandClaude Opus 5 157be0c049
Build & Deploy / Build & push images (push) Successful in 4m8s
Build & Deploy / Deploy to VM (push) Failing after 23s
Serve Firebase's auth handler from our own domain
Google sign-in fails in production: the popup opens, flashes, closes, and the
page shows a generic failure with nothing in the console or the network tab.

The app is served from drb.cusano.net while signInWithPopup opens its handler on
the project's firebaseapp.com origin. Chrome partitions third-party storage, so
the popup cannot read back the state its opener wrote and dies immediately.
Visiting the handler directly says so: "missing initial state ... a
storage-partitioned browser environment". Nothing about authorised domains or
the build was wrong -- the shipped bundle carries the correct apiKey and
authDomain, which is exactly what made this look like a code bug.

Caddy now proxies /__/auth/* on the bare domain to the Firebase Hosting origin,
rewriting Host so Firebase recognises the request. Same-site again, which is
Google's documented fix. The vhost becomes a `route` so the handler matches
before the catch-all proxy to Next.

The upstream host is a jinja default rather than a group_vars entry because
group_vars/all.yml is gitignored; override it there if the project ever moves.

Two manual steps remain, and all three parts are required or nothing changes:
the CI secret FIREBASE_AUTH_DOMAIN must become drb.cusano.net with a frontend
rebuild, and drb.cusano.net must be an authorised domain in the Firebase
console. This template also needs an ansible run -- CI alone will not deploy it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:59:07 -04:00

66 lines
3.1 KiB
Caddyfile

# Managed by Ansible — do not edit manually.
api.{{ domain }} {
# MQTT auth is no longer an HTTP backend c2-core exposes (it moved to
# mosquitto's own built-in dynamic-security plugin, administered over MQTT
# control topics — see app/internal/dynsec.py) — there is currently no
# /internal/* route in c2-core at all. This block stays anyway as defence
# in depth: c2-core's app-wide reverse_proxy below forwards every path by
# default, so this guarantees any FUTURE /internal/* route (or a
# regression that reintroduces one) is still unreachable from the public
# internet unless someone also deliberately deletes this block. `route`
# forces top-to-bottom evaluation instead of Caddy's automatic directive
# sorting, so this is guaranteed to run before reverse_proxy.
route {
respond /internal/* 404
reverse_proxy localhost:8888 {
header_up X-Forwarded-For {remote_host}
}
}
}
# mqtt.{{ domain }} has no application behind it — mosquitto's TLS listener
# (8883) is a raw MQTT socket, not HTTP, so Caddy can't reverse_proxy to it.
# This block's only job is to make Caddy request+manage a Let's Encrypt cert
# for the name via ACME HTTP-01, which infra/ansible's cert-sync unit then
# copies out to mosquitto. The DNS A record for mqtt.{{ domain }} must exist
# before this runs, or ACME issuance fails (see MQTT-PUBLIC-AUTH-PLAN.md).
mqtt.{{ domain }} {
respond 404
}
# Frontend is served on the bare domain, not app.{{ domain }}: only drb and api
# have public DNS records. A vhost for a name with no A record still starts,
# but Caddy retries ACME against it forever and logs a failure each time.
# To move it to app.{{ domain }}, create the A record first, then change this
# line — the reverse_proxy target stays the same either way.
{{ domain }} {
route {
# Firebase Auth's sign-in handler, served from our own origin.
#
# signInWithPopup opens {{ firebase_auth_handler_host | default('discord-radio-bot-461301.firebaseapp.com') }}/__/auth/handler and
# then reads back state the opener wrote. Chrome now partitions third-party
# storage, so when that handler is on a different site from the app the
# popup cannot see that state: it opens, fails, and closes instantly with no
# console or network trace. The handler page says so itself if you visit it
# directly ("storage-partitioned browser environment").
#
# Proxying the handler through this domain makes it same-site, which is
# Google's documented fix. Host must be rewritten upstream or Firebase
# Hosting will not recognise the request.
#
# NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN must be set to {{ domain }} in the CI
# build secrets to match, and {{ domain }} must be listed in the Firebase
# console's authorised domains. Changing only one of the three does nothing.
handle /__/auth/* {
reverse_proxy https://{{ firebase_auth_handler_host | default('discord-radio-bot-461301.firebaseapp.com') }} {
header_up Host {{ firebase_auth_handler_host | default('discord-radio-bot-461301.firebaseapp.com') }}
}
}
reverse_proxy localhost:3000 {
header_up X-Forwarded-For {remote_host}
}
}
}