OAuth Provider Setup
Stand up OAuth sign-in for a Mallnline environment (local, beta, prod) end-to-end: provider console, backend env, nginx proxy, and frontend wiring. The full ceremony lives across four places that all have to agree byte-for-byte โ get one wrong and you get either redirect_uri_mismatch from the provider or a silent redirect to your laptop after sign-in.
This is the dev-side companion to Auth & Identity โ that doc explains what the auth service does; this one covers how to wire a new environment in.
The Four Things That Must Agree
Every OAuth provider check hinges on byte-exact agreement across four places:
| Where | What it stores | Example (beta) |
|---|---|---|
| Provider console (Google Cloud / Apple Developer / X Dev Portal / Meta for Developers) | Authorized redirect URIs list |
https://beta.mallnline.com/auth/oauth/google/callback |
Backend env (.env.staging, .env.production, apps/auth/.env) |
<PROVIDER>_REDIRECT_URI |
GOOGLE_REDIRECT_URI=https://beta.mallnline.com/auth/oauth/google/callback |
nginx vhost (ngwenya-infra/ansible/vars/services.yml) |
/auth/oauth/ location proxied to auth (port 3008) |
extra_locations: { path: /auth/oauth/, target_port: 3008 } |
Frontend nav (ngwenya-front/src/lib/services/auth.ts) |
OAuth start URL it sends users to | ${AUTH_API_URL}/auth/oauth/google (i.e. /api/auth/auth/oauth/google) |
If any of the four diverges, the flow breaks. The provider does a string comparison, not a normalisation โ http:// โ https://, trailing slash matters, port number matters, case matters.
Architecture in 30 Seconds
The Mallnline frontend owns the /auth UI; the auth subgraph (Rust) owns the OAuth ceremony. They meet at the browser's origin via nginx path-mounted proxies:
Browser โโโบ /auth?mode=signup โโบ frontend (SvelteKit @ :4173)
Browser โโโบ /api/auth/auth/oauth/<p> โโบ auth service (:3008) [strip /api/auth/]
Provider โโโบ /auth/oauth/<p>/callback โโบ auth service (:3008) [keep prefix]
The split lets us register one callback URI per environment with each provider (/auth/oauth/<provider>/callback) while still letting the frontend own the user-facing /auth page. New environments inherit this layout for free โ see Hybrid Staging Infrastructure for the nginx wiring.
Adding a New Environment โ Checklist
You're standing up staging, preview-42, prod, whatever. For each provider you want enabled:
1. Register a Callback URI in the Provider Console
Provider-specific console steps are spelled out in the in-repo docs/OAUTH_SETUP.md (Apple/X/Facebook) and the Auth subgraph guide (Google). The invariant: the Authorized redirect URI must be exactly:
https://<host>/auth/oauth/<provider>/callback
โ and the Authorized JavaScript origin (Google only) must be:
https://<host>
CAUTION
Multiple OAuth clients across multiple Google projects is the most common foot-gun. If you have both an ngwenya and an ngwenya-federation Google Cloud project (we did), confirm which client ID is in .env.<environment> before editing redirect URIs. Editing the wrong client's URI list is a silent no-op โ the backend keeps using the other client and the provider keeps rejecting.
2. Set the Backend Env Vars
In the environment's env file (gitignored โ copy from .env.staging.example):
# Provider credentials (rotate via provider console; never commit real values)
GOOGLE_CLIENT_ID=<public โ fine in .env.*.example>
GOOGLE_CLIENT_SECRET=<__FILL_IN__>
GOOGLE_REDIRECT_URI=https://<host>/auth/oauth/google/callback
# Base URL of the user-facing frontend โ used by auth service to build
# post-flow redirects (OAuth โ /onboarding, magic-link emails, SAML ACS).
# WITHOUT THIS, apps/auth/src/config.rs falls back to http://localhost:5173
# and your staging users get redirected to your laptop after sign-in.
PLATFORM_URL=https://<host>
PLATFORM_URL is the one most often forgotten. Default is http://localhost:5173, which silently does the wrong thing in any non-local environment. The auth service uses it in eight places โ OAuth onboarding redirect, MFA challenge redirect, magic-link email body, SAML ACS URL builder โ so every flow ends up at the wrong host until it's set.
For the full env-var inventory, scan the tracked .env.staging.example (real .env.staging is gitignored to keep secrets out of source control).
3. Confirm the nginx Proxy
For browser โ backend traffic, the staging nginx vhost has to route /auth/oauth/* to the auth service. Verify in ngwenya-infra/ansible/vars/services.yml:
- name: app
subdomain: "<host-subdomain>"
tunnel_port: 4173 # frontend
extra_locations:
- path: /api/auth/
target_port: 3008
strip_prefix: true # /api/auth/me โ /me on the auth service
- path: /auth/oauth/
target_port: 3008 # keeps prefix so provider callbacks match
Both locations are required:
/api/auth/(prefix stripped) is what the frontend calls for REST endpoints (/me, email OTP, passkey, MFA)./auth/oauth/(prefix kept) is where providers redirect users back to. It must keep the prefix because Google/Apple/X/Facebook know it asโฆ/auth/oauth/<p>/callbackverbatim.
After editing, push the change with make deploy-all from ngwenya-infra/ansible/ โ it runs build-changed โ 01-deploy-hybrid โ bootstrap-vps with --tags nginx,certbot, and the nginx template self-emits SSL so HTTPS survives re-runs.
4. Recreate the Auth Container
Container picks up env changes only on recreate:
cd ngwenya-federation
podman compose -f docker-compose-staging.yaml \
--env-file .env.staging up -d --force-recreate auth
Substitute docker for podman if that's your runtime (check ansible/inventory/group_vars/mac.yml: container_cmd:).
5. Smoke Test
# Backend health
curl -sI https://<host>/api/auth/me # expect 401 (unauth, but reachable)
# OAuth start โ should 302 to accounts.google.com with redirect_uri matching .env
curl -sI 'https://<host>/api/auth/auth/oauth/google?redirect_uri=https%3A%2F%2F<host>%2Flobby' \
| grep -i '^location'
Then in an Incognito window (providers cache failures aggressively), hit https://<host>/auth?mode=signup and click the provider button. You should land back at https://<host>/onboarding?redirect=โฆ โ not at localhost:5173.
Common Pitfalls (each one cost โฅ 30 min of debugging)
`Error 400: redirect_uri_mismatch` from the provider
The string in <PROVIDER>_REDIRECT_URI doesn't byte-match anything registered in the provider's console. Diff them character by character โ most often it's https:// vs http://, a trailing slash, or you edited the other OAuth client's list (see the caution above). Provider config changes propagate in seconds; no redeploy needed.
After successful OAuth, user lands on `localhost:5173/onboarding`
PLATFORM_URL isn't set in the env file. Auth service falls back to the local default. Fix in env, recreate the container.
Frontend nav clicks `/auth/oauth/google` and hits SvelteKit 404
nginx's /auth/oauth/ location is missing or proxying to the wrong port. Verify with sudo nginx -T 2>/dev/null | grep -A 3 'location /auth/oauth/' on the VPS โ the proxy_pass should hit 127.0.0.1:3008/auth/oauth/.
`/auth/oauth/google/callback` returns the SvelteKit 404 page
Same as above. Frequently caused by /auth/ (whole prefix) being proxied instead of /auth/oauth/ (just OAuth) โ the broader prefix collides with the SvelteKit /auth UI route. The correct shape is in services.yml step 3 above.
Session cookie set but next page request looks logged-out
Cookie domain is wrong. The auth service sets Domain=<host> based on the request's Host header. If COOKIE_SECURE=true is set but the request came over HTTP (e.g. a tunnel without SSL termination), the browser rejects the cookie silently. Confirm COOKIE_SECURE matches whether the environment serves HTTPS, and that the nginx vhost has the SSL block emitted.
A secret leaked in source control
Treat it as compromised even in a private repo. Workflow: (a) rotate immediately in the provider console; (b) confirm .env.<environment> is gitignored and a tracked .env.<environment>.example exists; (c) if the leak hit a public repo, also run git filter-repo to scrub history โ but the real fix is always rotation, history rewrites are theater after that.
Where Each Provider Lands
For environments served behind beta.mallnline.com, the registered callback URIs always follow the same pattern. If you're standing up a new environment, copy this table and substitute the host:
| Provider | Authorized redirect URI | Authorized JS origin | Secret env var |
|---|---|---|---|
https://<host>/auth/oauth/google/callback |
https://<host> |
GOOGLE_CLIENT_SECRET |
|
| Apple | https://<host>/auth/oauth/apple/callback |
n/a | APPLE_CLIENT_SECRET (JWT) |
| X | https://<host>/auth/oauth/twitter/callback |
n/a | TWITTER_CLIENT_SECRET |
https://<host>/auth/oauth/facebook/callback |
n/a | FACEBOOK_CLIENT_SECRET |
Each provider has its own setup ceremony (Apple needs a Services ID + Sign In with Apple capability, X needs the app's read+write scope, etc.). Step-by-step provider screens live in OAUTH_SETUP.md in the federation repo.
Related
- Auth & Identity โ Auth subgraph internals, session lifecycle, SAML SSO, full endpoint inventory.
- MFA & Passkeys โ The MFA flow that may interrupt the post-OAuth redirect (the auth service hands off to
/auth/mfawhen 2FA is required). - Hybrid Staging Infrastructure โ The nginx + tunnel layer that makes the
/api/auth/and/auth/oauth/proxies work. - Staging Environment Architecture โ Where each container runs and how env files are loaded.
- Privacy & Security APIs โ Cookie hardening, CSRF posture, session revocation.