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_uri by 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 the aws destination.

A.2 — Deploy

From the HAP repo root:

wbsp-app deploy --config wbsp.yaml --destination aws

The 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-)
ThingValue
Auth base URLhttps://auth.wbsp-demo.com
Tenant slug / idwbsp / 6d8e672b-d5ab-494f-a53a-120681b1ce16
Admin headerX-Admin-Key: $ADMIN_KEY
Tenant headerX-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.tool

Field notes:

  • application_typeweb or machine_to_machine for confidential (server-side) clients; spa/native for public PKCE clients.
  • confidential: true — issues a client_secret. Server-rendered apps (Next.js/Auth.js) want this.
  • scopesopenid profile email is the usual set. Add offline_access if 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=true

2. 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.

⚠️ PATCH replaces 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.tool

Locate 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.tool

Success = JSON echoing the client_id with no error.


Verify

  1. 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
  2. Live authorize — a request with the new redirect_uri should return 302 (redirect to login), not invalid_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"
  3. 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

SymptomCause / fix
redirect_uri does not match a registered URIThe 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 callsADMIN_KEY empty or wrong — re-export it from .env.aws.
UntrustedHost / wrong callback host from the appSet 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 oneNew target's callback never registered — that's exactly §2.
client_secret lostYou can't re-read it; rotate via POST /v1/applications/{id}/rotate-secret and update the app's env.