Developer Docs

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>/callback verbatim.

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.

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
Google 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
Facebook 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.