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_typespaornativefor public clients (PKCE only),webormachine_to_machinefor confidential clients (a one-timeclient_secretis returned). redirect_urismust match exactly at authorize time.
Step 1 — Discover
Point your OIDC client library at:
http://localhost:8000/t/acme/.well-known/openid-configurationIt 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_tokenyour 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)
PYSend 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/jwksVerify 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_requiredas normal flow signals, not errors.
See also
- Getting Started (end-to-end demo) · Technical overview ·
Detailed user guide · API contracts:
specs/001-headless-auth-platform/contracts/