Integration Guide

How an application integrates with the Headless Auth Platform to log a user in and receive verifiable tokens. This is the standard OpenID Connect authorization-code flow with PKCE. By the end you will have completed a real login-to-token round trip.

Assumes a running instance (see Installation guide) reachable at http://localhost:8000, and an admin key (HAP_ADMIN_API_KEY). Endpoints are per-tenant under /t/{tenant_slug}/….

Concepts in 30 seconds

  • Tenant — your isolated environment (its own users, keys, settings).
  • Application — the client that receives tokens (gets a client_id).
  • Discovery document…/.well-known/openid-configuration; your OIDC client library reads it to self-configure.
  • PKCE — a required proof (code_verifier / code_challenge) that protects the authorization code.
  • Tokens — an ID token (who the user is), an access token (what they may do), and a refresh token (to get new ones).

Step 0 — Provision a tenant and an application (admin, one-time)

# Create a tenant; note its "id" and "slug".
curl -s -X POST localhost:8000/v1/tenants \
  -H "X-Admin-Key: $HAP_ADMIN_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Acme","slug":"acme","password_policy":{"min_length":8}}'

# Create an application under that tenant; note its "client_id".
curl -s -X POST localhost:8000/v1/applications \
  -H "X-Admin-Key: $HAP_ADMIN_API_KEY" -H "X-Tenant-Id: <TENANT_ID>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Web","application_type":"spa",
       "redirect_uris":["https://app.acme.example/callback"],
       "scopes":["openid","profile","email"]}'
  • Use application_type spa or native for public clients (PKCE only), web or machine_to_machine for confidential clients (a one-time client_secret is returned).
  • redirect_uris must match exactly at authorize time.

Step 1 — Discover

Point your OIDC client library at:

http://localhost:8000/t/acme/.well-known/openid-configuration

It returns the authorize/token/user-info/JWKS URLs, the supported grant types (authorization_code, refresh_token, client_credentials), and code_challenge_methods_supported: ["S256"].

Step 2 — Authenticate the user

Your application renders the login UI and authenticates the user. With a username/password front end:

# Register (your app's sign-up screen calls this)
curl -s -X POST localhost:8000/v1/auth/register \
  -H "X-Tenant-Id: <TENANT_ID>" -H "Content-Type: application/json" \
  -d '{"email":"user@acme.example","password":"a strong passphrase"}'

# Log in → returns a short-lived login_token your app passes to /authorize
curl -s -X POST localhost:8000/v1/auth/login \
  -H "X-Tenant-Id: <TENANT_ID>" -H "Content-Type: application/json" \
  -d '{"email":"user@acme.example","password":"a strong passphrase"}'

Passwordless (email OTP) — recommended for most apps. Instead of a password, request a one-time code and verify it; the verify call returns the same login_token. (Requires email delivery configured — see the Installation Guide's "Email / OTP delivery"; per-tenant just-in-time provisioning can auto-create the identity on first verify.)

# Send a code (uniform 202 regardless of whether the email exists — no account enumeration)
curl -s -X POST localhost:8000/v1/auth/otp/request \
  -H "X-Tenant-Id: <TENANT_ID>" -H "Content-Type: application/json" \
  -d '{"channel":"email","email":"user@acme.example"}'

# Verify the emailed code → returns a short-lived login_token (same as password login)
curl -s -X POST localhost:8000/v1/auth/otp/verify \
  -H "X-Tenant-Id: <TENANT_ID>" -H "Content-Type: application/json" \
  -d '{"channel":"email","email":"user@acme.example","code":"123456"}'

Passkeys, social login, enterprise SSO, and magic links are further ways to authenticate — see the Detailed user guide. In every case the result is the same login_token your app carries into the authorize step.

Step 3 — Authorize (with PKCE)

Create a PKCE pair:

python - <<'PY'
import os, base64, hashlib
v = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
c = base64.urlsafe_b64encode(hashlib.sha256(v.encode()).digest()).rstrip(b"=").decode()
print("code_verifier =", v); print("code_challenge =", c)
PY

Send the user to the authorize endpoint (here driven headlessly with the login_token); the platform replies with a redirect to your redirect_uri carrying a one-time code:

curl -si "localhost:8000/t/acme/oauth2/authorize?response_type=code\
&client_id=<CLIENT_ID>&redirect_uri=https://app.acme.example/callback\
&scope=openid%20profile%20email&code_challenge=<CHALLENGE>\
&code_challenge_method=S256&login_token=<LOGIN_TOKEN>&consent=granted" | grep -i location
# Location: https://app.acme.example/callback?code=<CODE>&...

If the session's assurance level is too low for a required action, the platform responds with interaction_required and required_acr — run an MFA step-up, then retry (see the detailed guide).

Step 4 — Exchange the code for tokens

curl -s -X POST localhost:8000/t/acme/oauth2/token \
  -d grant_type=authorization_code -d code=<CODE> \
  -d redirect_uri=https://app.acme.example/callback \
  -d code_verifier=<VERIFIER> -d client_id=<CLIENT_ID>
# → { "access_token": "...", "id_token": "...", "refresh_token": "...", "token_type": "Bearer", ... }

Confidential clients additionally send client_secret (or HTTP Basic auth).

Step 5 — Use and verify tokens

# Fetch the user's profile with the access token
curl -s localhost:8000/t/acme/oauth2/userinfo -H "Authorization: Bearer <ACCESS_TOKEN>"

# Your API/services verify access tokens against the tenant's public keys
curl -s localhost:8000/t/acme/oauth2/jwks

Verify ID/access tokens by checking the signature against the JWKS, plus iss, aud, and exp. Most OIDC libraries do this for you once pointed at the discovery URL.

Step 6 — Keep the session fresh & end it

# Rotate tokens (the old refresh token becomes invalid; reuse triggers family revocation)
curl -s -X POST localhost:8000/t/acme/oauth2/token \
  -d grant_type=refresh_token -d refresh_token=<REFRESH_TOKEN> -d client_id=<CLIENT_ID>

# Revoke a token (logout)
curl -s -X POST localhost:8000/t/acme/oauth2/revoke \
  -d token=<TOKEN> -d client_id=<CLIENT_ID>

Machine-to-machine (no user)

For service-to-service calls, use a confidential application and the client-credentials grant:

curl -s -X POST localhost:8000/t/acme/oauth2/token \
  -d grant_type=client_credentials -d client_id=<CLIENT_ID> -d client_secret=<SECRET>

SDKs & the API contract

Any standards-compliant OIDC/OAuth client library works (point it at the discovery URL). The full machine-readable API description is at target/openapi/openapi.json (OpenAPI 3.1) — import it into Postman/Insomnia or generate typed client SDKs from it.

Good practices

  • Use a confidential client and a server-side token exchange for traditional web apps; use a public client + PKCE for SPAs and native apps.
  • Request only the scopes you need; store refresh tokens securely.
  • Treat interaction_required / consent_required as normal flow signals, not errors.

See also