fefeat: supabase add secrets and change to just ios native auth
This commit is contained in:
parent
67e81d39f3
commit
5c6b4f18eb
5 changed files with 134 additions and 205 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ++ [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue