fefeat: supabase add secrets and change to just ios native auth

This commit is contained in:
Dominik Polakovics Polakovics 2026-04-24 18:57:09 +02:00
parent 67e81d39f3
commit 5c6b4f18eb
5 changed files with 134 additions and 205 deletions

View file

@ -1,4 +1,4 @@
# Supabase auth setup: Google + Apple OAuth, fueltide.io email
# Supabase auth setup: Google OAuth, Apple native sign-in (iOS), fueltide.io email
This doc lists the **user-side steps** required to make the code changes in
this branch functional. Nothing here is performed by Nix — these are manual
@ -6,20 +6,28 @@ actions on external services, LDAP, SOPS, and DNS.
The Nix changes in this branch cover:
- `hosts/web-arm/modules/supabase/default.nix` — GoTrue env for Google + Apple
OAuth, SMTP pointed at `mail.cloonar.com:587`, `MAILER_AUTOCONFIRM=false`,
`SITE_URL` + `URI_ALLOW_LIST` for fueltide.io, python+cryptography in the
env-generate path (for Apple JWT signing).
- `hosts/web-arm/modules/supabase/env-generate.sh` — new `auth.env` block that
pulls SMTP + OAuth creds from SOPS and signs the Apple client-secret JWT
fresh on every activation.
- `hosts/mail/modules/dkim-fueltide.nix` — installs a per-domain DKIM key for
fueltide.io into rspamd so outbound mail from `noreply@fueltide.io` is
- `hosts/web-arm/modules/supabase/default.nix` — GoTrue env for Google OAuth
(web code-exchange flow) and Apple native sign-in (iOS id_token flow,
`GOTRUE_EXTERNAL_APPLE_CLIENT_ID=io.fueltide.workout`), SMTP pointed at
`mail.cloonar.com:587`, `MAILER_AUTOCONFIRM=false`, `SITE_URL` +
`URI_ALLOW_LIST` for fueltide.io.
- `hosts/web-arm/modules/supabase/env-generate.sh` — new `auth.env` block
that pulls SMTP + Google creds from SOPS.
- `hosts/mail/modules/dkim-fueltide.nix` — installs a per-domain DKIM key
for fueltide.io into rspamd so outbound mail from `noreply@fueltide.io` is
signed.
Complete the seven steps below **before** merging to master. Merging without
them will deploy a broken GoTrue (missing OAuth/SMTP creds → auth emails fail,
OAuth flows 500).
Apple sign-in is scoped to the **native iOS flow only**: the app uses
`AuthenticationServices` to obtain an Apple `id_token`, then calls
`supabase.auth.signInWithIdToken({ provider: 'apple', token, nonce })`.
GoTrue verifies the id_token against Apple's JWKS and checks that `aud`
matches `io.fueltide.workout`. No server-side client secret, `.p8` key, or
Services ID is needed. Android uses native Google sign-in (handled
separately) and no Apple browser flow is supported.
Complete the six steps below **before** merging to master. Merging without
them will deploy a broken GoTrue (missing Google/SMTP creds → auth emails
fail, Google OAuth flows 500).
---
@ -97,7 +105,7 @@ rspamd-dkim-fueltide-io-key: |
nix-shell -p sops --run 'sops hosts/web-arm/secrets.yaml'
```
Inside the existing `supabase-env` multiline value, append eight new lines
Inside the existing `supabase-env` multiline value, append four new lines
(these are sourced as shell variables by `env-generate.sh`):
```
@ -105,20 +113,6 @@ SMTP_USER=supabase@cloonar.com
SMTP_PASS=<plaintext from step 1>
GOOGLE_CLIENT_ID=<from step 5>
GOOGLE_SECRET=<from step 5>
APPLE_TEAM_ID=XWJ4DC7TBH
APPLE_KEY_ID=<from step 6>
APPLE_SERVICES_ID=com.cloonar.supabase.fueltide
APPLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n<.p8 body>\n-----END PRIVATE KEY-----
```
Note on `APPLE_PRIVATE_KEY`: it must be **one line** with literal backslash-n
separating the PEM lines (no real newlines inside the value). The python
signer in `env-generate.sh` un-escapes those via `decode("unicode_escape")`
before loading the PEM. To format an existing `AuthKey_XXX.p8` as that single
line:
```bash
awk '{printf "%s\\n", $0}' AuthKey_XXXXXXXXXX.p8
```
## 4. DNS records for `fueltide.io`
@ -148,45 +142,19 @@ won't route — acceptable for one-way transactional mail. Add an MX pointing at
3. Copy Client ID + Client Secret → into SOPS as `GOOGLE_CLIENT_ID` and
`GOOGLE_SECRET`.
## 6. Apple Developer Sign in with Apple (≈ 15 min, paid account required)
## 6. Apple Developer — enable Sign in with Apple on the iOS App ID
1. developer.apple.com → **Certificates, IDs & Profiles → Identifiers → +
→ Services IDs**. Description `Fueltide Supabase Auth`. Identifier
`com.cloonar.supabase.fueltide`. Check **Sign in with Apple → Configure**.
2. Primary App ID: existing `io.fueltide.workout` (Team `XWJ4DC7TBH`, see
`hosts/web-arm/sites/fueltide.io.nix`). Domains and Subdomains:
`supabase.cloonar.com`. Return URLs:
`https://supabase.cloonar.com/auth/v1/callback`. Save.
3. **Keys → +** → name `Fueltide Supabase Auth` → check **Sign in with Apple
→ Configure** → primary App ID `io.fueltide.workout`. Register.
4. **Download the `.p8` file now** — Apple only offers it once.
5. Note the Key ID (10 chars) displayed on the key page.
6. Team ID is `XWJ4DC7TBH` (already known).
7. Into SOPS on web-arm:
- `APPLE_TEAM_ID=XWJ4DC7TBH`
- `APPLE_KEY_ID=<from step 5>`
- `APPLE_SERVICES_ID=com.cloonar.supabase.fueltide`
- `APPLE_PRIVATE_KEY=<single-line .p8 as described in step 3>`
Only one action, no keys or Services IDs:
### iOS native flow (optional)
1. developer.apple.com → **Certificates, IDs & Profiles → Identifiers → App
IDs**. Select `io.fueltide.workout` (Team `XWJ4DC7TBH`, see
`hosts/web-arm/sites/fueltide.io.nix`). Check **Sign in with Apple**.
Save.
If the fueltide iOS app will use `supabase.auth.signInWithIdToken({ provider:
'apple', token: identityToken })` (native `AuthenticationServices` SDK, no web
browser), the iOS bundle ID must also appear in `GOTRUE_EXTERNAL_APPLE_CLIENT_ID`.
Change the line in `env-generate.sh` that currently reads:
```sh
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-}
```
to something like:
```sh
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-},io.fueltide.workout
```
(GoTrue accepts a comma-separated audiences list here and validates incoming
id_tokens against any of them.)
That's it on the Apple side. No Services ID, no Keys, no `.p8` download.
The iOS app obtains the `id_token` on-device via `AuthenticationServices`
and posts it to `supabase.auth.signInWithIdToken`; GoTrue validates it
against Apple's JWKS with `aud=io.fueltide.workout`.
## 7. Merge and deploy
@ -204,16 +172,18 @@ Bento rolls out both hosts. On `web-arm.cloonar.com`:
```bash
sudo systemctl restart supabase-env-generate
sudo cat /run/supabase/auth.env # expect 8 new vars populated
sudo cat /run/supabase/auth.env # expect SMTP + Google vars populated
sudo podman exec supabase-auth nc -vz mail.cloonar.com 587
sudo podman restart supabase-auth
```
### Verification checklist
- [ ] `/run/supabase/auth.env` contains `GOTRUE_EXTERNAL_APPLE_SECRET=<long-JWT>`.
- [ ] Second `systemctl restart supabase-env-generate` produces a different
Apple JWT (freshness — signed with new `iat`).
- [ ] `/run/supabase/auth.env` contains `GOTRUE_SMTP_USER`, `GOTRUE_SMTP_PASS`,
`GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID`, `GOTRUE_EXTERNAL_GOOGLE_SECRET`.
- [ ] `podman inspect supabase-auth` shows
`GOTRUE_EXTERNAL_APPLE_ENABLED=true` and
`GOTRUE_EXTERNAL_APPLE_CLIENT_ID=io.fueltide.workout` in the env.
- [ ] `curl -X POST -H 'apikey: <anon>' -H 'Content-Type: application/json' \
https://supabase.cloonar.com/auth/v1/signup \
-d '{"email":"<real inbox>","password":"correct horse battery staple"}'`
@ -225,17 +195,16 @@ sudo podman restart supabase-auth
`https://supabase.cloonar.com/auth/v1/authorize?provider=google`
completes and lands on `/auth/v1/callback`. Row in `auth.identities`
with `provider='google'`.
- [ ] Same with `?provider=apple` from a page Apple's Return URL accepts.
- [ ] From the iOS app: Sign in with Apple →
`supabase.auth.signInWithIdToken({ provider: 'apple', token, nonce })`
succeeds. Row in `auth.identities` with `provider='apple'` and
`identity_data.sub` matching the Apple user id. (Apple sign-in has no
browser flow here — it is tested from the app only.)
- [ ] Send a signup to [mail-tester.com](https://www.mail-tester.com/) — target
≥ 9/10 spam score.
## Rotation notes
- **Apple client-secret JWT**: auto-regenerated on every activation
(`supabase-env-generate.service`). No manual rotation.
- **Apple `.p8` key**: no expiry, but revoking it in the Apple console
immediately breaks auth. If ever rotated, update `APPLE_KEY_ID` and
`APPLE_PRIVATE_KEY` in SOPS together.
- **Google client secret**: no expiry; rotate via Google Cloud console if
leaked and update `GOOGLE_SECRET` in SOPS.
- **DKIM key**: no expiry, but best practice is to rotate yearly. Rotation

View file

@ -70,12 +70,7 @@ in
supabase-env-generate = {
description = "Generate Supabase per-container env files from SOPS secrets";
wantedBy = [ "multi-user.target" ];
# python+cryptography is used to sign the Apple OAuth client-secret JWT
# (ES256) inside env-generate.sh.
path = [
pkgs.jq
(pkgs.python3.withPackages (ps: [ ps.cryptography ]))
];
path = [ pkgs.jq ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
@ -226,7 +221,7 @@ in
GOTRUE_EXTERNAL_GOOGLE_ENABLED = "true";
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI = "https://supabase.cloonar.com/auth/v1/callback";
GOTRUE_EXTERNAL_APPLE_ENABLED = "true";
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI = "https://supabase.cloonar.com/auth/v1/callback";
GOTRUE_EXTERNAL_APPLE_CLIENT_ID = "io.fueltide.workout";
};
environmentFiles = [ "/run/supabase/auth.env" ];
extraOptions = supabaseNet ++ [

View file

@ -22,40 +22,6 @@ LOGFLARE_PRIVATE_ACCESS_TOKEN=$LOGFLARE_PRIVATE_ACCESS_TOKEN
POSTGRES_BACKEND_URL=postgresql://supabase_admin:$PG_PASS_ENCODED@db:5432/_supabase
EOF
# Apple client-secret is a short-lived JWT signed with the .p8 key downloaded
# from Apple Developer. Re-sign on every activation (lifetime 180 days, Apple's
# cap) so there is no manual rotation ritual. The SOPS-sourced APPLE_PRIVATE_KEY
# is stored as a single line with literal \n separators; python un-escapes it.
APPLE_SECRET=""
if [ -n "${APPLE_TEAM_ID:-}" ] && [ -n "${APPLE_KEY_ID:-}" ] \
&& [ -n "${APPLE_SERVICES_ID:-}" ] && [ -n "${APPLE_PRIVATE_KEY:-}" ]; then
APPLE_SECRET=$(
APPLE_TEAM_ID="$APPLE_TEAM_ID" \
APPLE_KEY_ID="$APPLE_KEY_ID" \
APPLE_SERVICES_ID="$APPLE_SERVICES_ID" \
APPLE_PRIVATE_KEY="$APPLE_PRIVATE_KEY" \
python3 - <<'PY'
import base64, json, os, time
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
now = int(time.time())
header = {"alg": "ES256", "kid": os.environ["APPLE_KEY_ID"], "typ": "JWT"}
payload = {"iss": os.environ["APPLE_TEAM_ID"], "iat": now, "exp": now + 86400 * 180,
"aud": "https://appleid.apple.com", "sub": os.environ["APPLE_SERVICES_ID"]}
parts = (b64u(json.dumps(header, separators=(",", ":")).encode())
+ "." + b64u(json.dumps(payload, separators=(",", ":")).encode())).encode()
pem = os.environ["APPLE_PRIVATE_KEY"].encode().decode("unicode_escape").encode()
key = serialization.load_pem_private_key(pem, password=None)
der = key.sign(parts, ec.ECDSA(hashes.SHA256()))
r, s = decode_dss_signature(der)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")
print(parts.decode() + "." + b64u(raw))
PY
)
fi
cat > /run/supabase/auth.env <<EOF
GOTRUE_JWT_SECRET=$JWT_SECRET
GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:$PG_PASS_ENCODED@db:5432/postgres
@ -63,8 +29,6 @@ GOTRUE_SMTP_USER=${SMTP_USER:-}
GOTRUE_SMTP_PASS=${SMTP_PASS:-}
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
GOTRUE_EXTERNAL_GOOGLE_SECRET=${GOOGLE_SECRET:-}
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=${APPLE_SERVICES_ID:-}
GOTRUE_EXTERNAL_APPLE_SECRET=$APPLE_SECRET
EOF
cat > /run/supabase/rest.env <<EOF

File diff suppressed because one or more lines are too long