215 lines
7.9 KiB
Markdown
215 lines
7.9 KiB
Markdown
# 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
|
||
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 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.
|
||
|
||
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).
|
||
|
||
---
|
||
|
||
## 1. LDAP service account + fueltide alias on `mail.cloonar.com`
|
||
|
||
Mirrors the `gitea@cloonar.com` / `authelia@cloonar.com` pattern. The alias
|
||
on `noreply@fueltide.io` is what `smtpd_sender_login_maps` uses to let the
|
||
`supabase` SASL user send as that address without tripping
|
||
`reject_authenticated_sender_login_mismatch`.
|
||
|
||
```bash
|
||
# on mail.cloonar.com
|
||
SMTP_PASS=$(openssl rand -base64 30 | tr -d '/+=' | head -c 32)
|
||
echo "SMTP_PASS (store this in SOPS, step 3): $SMTP_PASS"
|
||
CRYPT=$(mkpasswd -m sha-512 "$SMTP_PASS")
|
||
|
||
cat > /tmp/supabase.ldif <<EOF
|
||
dn: uid=supabase,ou=users,dc=cloonar,dc=com
|
||
objectClass: mailAccount
|
||
objectClass: inetOrgPerson
|
||
uid: supabase
|
||
cn: Supabase Auth
|
||
sn: Auth
|
||
mail: supabase@cloonar.com
|
||
mailSendOnly: TRUE
|
||
userPassword: {CRYPT}$CRYPT
|
||
description: SASL account for Supabase GoTrue outbound mail
|
||
|
||
dn: mail=noreply@fueltide.io,ou=aliases,dc=cloonar,dc=com
|
||
objectClass: mailAlias
|
||
mail: noreply@fueltide.io
|
||
maildrop: supabase@cloonar.com
|
||
EOF
|
||
|
||
ldapadd -x -D "cn=admin,dc=cloonar,dc=com" -W -f /tmp/supabase.ldif
|
||
rm /tmp/supabase.ldif
|
||
```
|
||
|
||
## 2. Generate the fueltide.io DKIM key
|
||
|
||
Selector is `default` to match the glob that rspamd's `dkim_signing` block
|
||
(`hosts/mail/modules/rspamd.nix:15-19`) watches for.
|
||
|
||
```bash
|
||
mkdir -p /tmp/dkim-gen && cd /tmp/dkim-gen
|
||
nix-shell -p rspamd --run \
|
||
"rspamadm dkim_keygen -s default -d fueltide.io -k fueltide.io.default.key"
|
||
|
||
# private key: fueltide.io.default.key -> goes into SOPS (step 3)
|
||
# public key: printed to stdout -> goes into DNS (step 4)
|
||
```
|
||
|
||
Wipe the temp dir once both are copied out.
|
||
|
||
## 3. SOPS edits (two files)
|
||
|
||
### `hosts/mail/secrets.yaml`
|
||
|
||
```bash
|
||
nix-shell -p sops --run 'sops hosts/mail/secrets.yaml'
|
||
```
|
||
|
||
Add:
|
||
|
||
```yaml
|
||
rspamd-dkim-fueltide-io-key: |
|
||
-----BEGIN PRIVATE KEY-----
|
||
<paste contents of /tmp/dkim-gen/fueltide.io.default.key>
|
||
-----END PRIVATE KEY-----
|
||
```
|
||
|
||
### `hosts/web-arm/secrets.yaml`
|
||
|
||
```bash
|
||
nix-shell -p sops --run 'sops hosts/web-arm/secrets.yaml'
|
||
```
|
||
|
||
Inside the existing `supabase-env` multiline value, append four new lines
|
||
(these are sourced as shell variables by `env-generate.sh`):
|
||
|
||
```
|
||
SMTP_USER=supabase@cloonar.com
|
||
SMTP_PASS=<plaintext from step 1>
|
||
GOOGLE_CLIENT_ID=<from step 5>
|
||
GOOGLE_SECRET=<from step 5>
|
||
```
|
||
|
||
## 4. DNS records for `fueltide.io`
|
||
|
||
Add on whichever DNS provider hosts fueltide.io:
|
||
|
||
```
|
||
TXT @ v=spf1 mx a:mail.cloonar.com ~all
|
||
TXT default._domainkey v=DKIM1; k=rsa; p=<public key from step 2>
|
||
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@cloonar.com; fo=1
|
||
```
|
||
|
||
PTR for mail.cloonar.com is already set (it's been sending for cloonar.com).
|
||
If fueltide.io has no MX record, outbound is fine but bounces from remote MTAs
|
||
won't route — acceptable for one-way transactional mail. Add an MX pointing at
|
||
`mail.cloonar.com.` if you want bounces to be received.
|
||
|
||
## 5. Google Cloud OAuth client (≈ 5 min)
|
||
|
||
1. console.cloud.google.com → **APIs & Services → OAuth consent screen**.
|
||
External user type. App name `Fueltide`, user support email, developer
|
||
contact. Scopes: `openid`, `email`, `profile`. Submit (or keep in testing
|
||
if only internal users).
|
||
2. **Credentials → Create Credentials → OAuth client ID → Web application**.
|
||
Name `Supabase`. Authorised redirect URI:
|
||
`https://supabase.cloonar.com/auth/v1/callback`.
|
||
3. Copy Client ID + Client Secret → into SOPS as `GOOGLE_CLIENT_ID` and
|
||
`GOOGLE_SECRET`.
|
||
|
||
## 6. Apple Developer — enable Sign in with Apple on the iOS App ID
|
||
|
||
Only one action, no keys or Services IDs:
|
||
|
||
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.
|
||
|
||
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
|
||
|
||
Once steps 1–6 are done:
|
||
|
||
```bash
|
||
./scripts/test-configuration web-arm
|
||
./scripts/test-configuration mail
|
||
git checkout master
|
||
git merge --no-ff <this-branch>
|
||
git push
|
||
```
|
||
|
||
Bento rolls out both hosts. On `web-arm.cloonar.com`:
|
||
|
||
```bash
|
||
sudo systemctl restart supabase-env-generate
|
||
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_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"}'`
|
||
delivers a mail with `From: noreply@fueltide.io` within ~30 s.
|
||
- [ ] Mail headers show `dkim=pass`, `spf=pass`, `dmarc=pass`
|
||
(`Authentication-Results` header).
|
||
- [ ] `POST /auth/v1/recover` triggers a reset mail.
|
||
- [ ] Browser visit to
|
||
`https://supabase.cloonar.com/auth/v1/authorize?provider=google`
|
||
completes and lands on `/auth/v1/callback`. Row in `auth.identities`
|
||
with `provider='google'`.
|
||
- [ ] 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
|
||
|
||
- **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
|
||
= regenerate keypair (step 2), replace the SOPS value (step 3), update DNS
|
||
(step 4), deploy. Keep both old+new DNS records live for 24h during
|
||
cutover.
|
||
- **SMTP LDAP password**: no expiry. To rotate, run `mkpasswd` again and
|
||
update both the LDAP userPassword attribute and SOPS `SMTP_PASS`.
|