HOWTO — Register an app with HAP (auth)
Every app that signs users in through the platform's auth server (HAP — the WBSP Headless
Auth Platform at auth.wbsp-demo.com) needs an OAuth application registered in HAP. That
registration holds the app's client_id/client_secret and the exact list of redirect URIs
HAP will allow it to return to after login.
Why this matters. HAP matches
redirect_uriby exact string — no wildcards, no path or port fuzzing. If an app is deployed to a new domain/destination whose callback URL isn't on the list, login fails with{"error":"invalid_request","error_description":"redirect_uri does not match a registered URI"}. Every new domain (e.g. a second deploy destination) needs its callback added — this is not automated at deploy time.
This guide covers:
- A — Deploy HAP itself (the auth server at
auth.wbsp-demo.com). - B — Register an app with HAP: provision an OAuth app (
client_id/client_secret) and manage its redirect URIs.
A — How to deploy HAP
HAP is a normal WBSP app: its repo is the Headless Auth Platform project
(<hap-repo> below — obtain its location from your platform operator), with wbsp.yaml at the
root (name: headless-auth). The platform provisions its Postgres + Redis and builds the image from
wbsp.Docker. You deploy it with the same wbsp-app deploy you use for any app.
A.1 — Secrets in .env.aws (set BEFORE deploying)
HAP declares two required secrets by reference (plus SES SMTP creds for email). They must exist in
the git-ignored .env.aws beside wbsp.yaml, or the deploy fails closed:
cd <hap-repo> # the Headless Auth Platform project root (ask your operator)
grep -E '^HAP_MASTER_ENCRYPTION_KEY=|^HAP_ADMIN_API_KEY=|^SES_SMTP_USERNAME=|^SES_SMTP_PASSWORD=' .env.aws \
| sed -E 's/=.*/=<set>/'HAP_MASTER_ENCRYPTION_KEY— ⚠️ must be the EXISTING value. It decrypts the tenant signing keys already stored in the AWS database. A new or missing value means HAP can no longer sign tokens → every existing login breaks. Never invent a new one for an existing deployment.HAP_ADMIN_API_KEY— the admin key used for all of section B. You may choose/rotate it freely (openssl rand -base64 32); it affects nothing else.SES_SMTP_USERNAME/SES_SMTP_PASSWORD— SES SMTP creds for OTP / magic-link email on theawsdestination.
A.2 — Deploy
From the HAP repo root:
wbsp-app deploy --config wbsp.yaml --destination awsThe database schema migrates automatically at container startup (alembic upgrade head) — never
touch the DB by hand. Routes to auth.wbsp-demo.com per the aws destination in wbsp.yaml.
A.3 — Enable Hosted Login for the wbsp tenant (once)
/hosted/* and OIDC login 404 until the shared wbsp tenant has the flags on. After the
prerequisites in section B are exported:
curl -s -X PATCH "$HAP/v1/tenants/$TENANT_ID" \
-H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"hosted_login_enabled": true, "passwordless_jit_provisioning": true}'A.4 — Verify the deploy
Should print 200 (the admin API only exists in a current build):
curl -s -o /dev/null -w "%{http_code}\n" "$HAP/v1/applications" \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID"B — Register an app with HAP
Now you'll provision an OAuth app and manage its redirect URIs.
Prerequisites — set these once per terminal
The admin key lives in the HAP repo's git-ignored .env.aws as HAP_ADMIN_API_KEY. Run from the
HAP repo root so it resolves:
cd <hap-repo> # the Headless Auth Platform project root (ask your operator)
export HAP=https://auth.wbsp-demo.com
export TENANT_ID=6d8e672b-d5ab-494f-a53a-120681b1ce16 # the shared "wbsp" tenant
export ADMIN_KEY=$(grep '^HAP_ADMIN_API_KEY=' .env.aws | tail -1 | cut -d= -f2-)| Thing | Value |
|---|---|
| Auth base URL | https://auth.wbsp-demo.com |
| Tenant slug / id | wbsp / 6d8e672b-d5ab-494f-a53a-120681b1ce16 |
| Admin header | X-Admin-Key: $ADMIN_KEY |
| Tenant header | X-Tenant-Id: $TENANT_ID |
Sanity check (should print 200):
curl -s -o /dev/null -w "%{http_code}\n" "$HAP/v1/applications" \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID"1. Provision a HAP app
Create the OAuth application with POST /v1/applications. The client_secret is returned once
— capture it immediately into the app's secret env (e.g. .env.<target>); it is never shown again.
Most platform web apps use Auth.js with a provider id of hap, so their callback path is
/api/auth/callback/hap. Substitute your app's real public domain.
curl -s -X POST "$HAP/v1/applications" \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID" -H "Content-Type: application/json" \
-d '{
"name": "My App",
"application_type": "web",
"redirect_uris": ["https://myapp.wbsp-demo.com/api/auth/callback/hap"],
"scopes": ["openid", "profile", "email"],
"confidential": true,
"access_mode": "open"
}' | python3 -m json.toolField notes:
application_type—webormachine_to_machinefor confidential (server-side) clients;spa/nativefor public PKCE clients.confidential: true— issues aclient_secret. Server-rendered apps (Next.js/Auth.js) want this.scopes—openid profile emailis the usual set. Addoffline_accessif the app needs refresh tokens.redirect_uris— the exact callback URL(s). Include a localhost entry too if you also run the app locally, e.g.http://localhost:3000/api/auth/callback/hap.
The response includes id (an internal uuid), client_id (e.g. hap_…), and client_secret.
Wire the app: put the credentials into the app's config — non-secrets in wbsp.yaml env,
the secret in .env.<target> (git-ignored). Typical Auth.js / HAP vars:
HAP_BASE_URL=https://auth.wbsp-demo.com
HAP_TENANT=wbsp
HAP_TENANT_ID=6d8e672b-d5ab-494f-a53a-120681b1ce16
HAP_<APP>_CLIENT_ID=hap_… # from the response
HAP_<APP>_CLIENT_SECRET=… # secret — .env.<target> only, never commit
AUTH_URL=https://myapp.wbsp-demo.com # pin so Auth.js builds correct callback URLs behind the proxy
AUTH_TRUST_HOST=true2. Register / update an app's redirect URIs
Use this whenever an app gains a new domain — most commonly a second deploy target (e.g. the CRM
adding a twist-crm.wbsp-demo.com alongside crm.wbsp-demo.com). You add the new callback to the
same application; both domains share one client_id.
⚠️
PATCHreplaces the whole array, it does not merge. Always read the current values first, then send the superset (old + new).
Step A — find the app's internal id and current URIs
curl -s "$HAP/v1/applications" \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID" | python3 -m json.toolLocate your app by name or client_id, then copy its id (the uuid — not the client_id)
and note its existing redirect_uris / post_logout_redirect_uris.
Step B — PATCH with old + new URIs
curl -s -X PATCH "$HAP/v1/applications/<APP_ID>" \
-H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID" -H "Content-Type: application/json" \
-d '{
"redirect_uris": [
"https://myapp.wbsp-demo.com/api/auth/callback/hap",
"https://new-domain.wbsp-demo.com/api/auth/callback/hap"
],
"post_logout_redirect_uris": [
"https://myapp.wbsp-demo.com/login",
"https://new-domain.wbsp-demo.com/login"
]
}' | python3 -m json.toolSuccess = JSON echoing the client_id with no error.
Verify
- Stored state — re-list and confirm both arrays contain every expected URI:
curl -s "$HAP/v1/applications" \ -H "X-Admin-Key: $ADMIN_KEY" -H "X-Tenant-Id: $TENANT_ID" | python3 -m json.tool - Live authorize — a request with the new
redirect_urishould return302(redirect to login), notinvalid_request:REDIR=$(python3 -c 'import urllib.parse;print(urllib.parse.quote("https://new-domain.wbsp-demo.com/api/auth/callback/hap"))') curl -s -o /dev/null -w "%{http_code}\n" \ "$HAP/t/wbsp/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=$REDIR&scope=openid+profile+email&code_challenge=x&code_challenge_method=S256" - Browser — open the app's URL in a fresh/incognito session; you should reach the HAP login page and, after signing in, land back on the app.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
redirect_uri does not match a registered URI | The exact callback isn't in redirect_uris. Check scheme (https), host, port, and path char-for-char; add it via §2. |
{"detail":"Not authenticated"} / 401 on admin calls | ADMIN_KEY empty or wrong — re-export it from .env.aws. |
UntrustedHost / wrong callback host from the app | Set AUTH_URL to the app's public URL and AUTH_TRUST_HOST=true (the pod sees 0.0.0.0:8080 behind the proxy). |
| Login worked on one domain but not a new one | New target's callback never registered — that's exactly §2. |
client_secret lost | You can't re-read it; rotate via POST /v1/applications/{id}/rotate-secret and update the app's env. |