Getting Started

First OIDC Client

A complete walkthrough of the Authorization Code flow with PKCE — from redirect to tokens to UserInfo. Covers client registration, token introspection, refresh tokens, and common gotchas.

The Flow at a Glance

User ──────────> Your App  "Login" click
Your App ──────────> Browser  302 → /acme/authorize?...&code_challenge=S256
Browser ──────────> Clavex  GET /acme/authorize (login UI)
Clavex ──────────> Browser  302 → /callback?code=AUTH_CODE&state=...
Your App ──────────> Clavex  POST /acme/token (code + verifier)
Clavex ──────────> Your App  access_token + id_token + refresh_token
Your App ──────────> Clavex  GET /acme/userinfo (Bearer access_token)

Step 1 — Discovery

Every Clavex tenant publishes an OpenID Connect discovery document. Start here — your library will automatically pick up all endpoints.

bash
$ curl -s https://id.clavex.eu/acme/.well-known/openid-configuration | jq '{issuer,authorization_endpoint,token_endpoint,userinfo_endpoint,jwks_uri}' { "issuer": "https://id.clavex.eu/acme", "authorization_endpoint": "https://id.clavex.eu/acme/authorize", "token_endpoint": "https://id.clavex.eu/acme/token", "userinfo_endpoint": "https://id.clavex.eu/acme/userinfo", "jwks_uri": "https://id.clavex.eu/acme/.well-known/jwks.json" }

Step 2 — Client Registration

Register your application via the Admin API. The token_endpoint_auth_method determines how the client authenticates at the token endpoint.

auth_methodWhen to useRequires
noneSPAs, mobile apps (public clients)PKCE mandatory
client_secret_postServer-side apps (simple setup)client_secret in body
client_secret_basicServer-side apps (HTTP Basic)client_secret in header
private_key_jwtHigh-security / FAPI profilesRS256/ES256 key pair
bash
$ curl -s -X POST https://id.clavex.eu/api/v1/organizations/$ORG_ID/clients \ -H "Authorization: Bearer $ORG_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Web App", "redirect_uris": ["https://app.example.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types":["code"], "scope": "openid email profile", "token_endpoint_auth_method": "none" }' | jq '{client_id}' { "client_id": "clavex_01925..." }

Step 3 — Authorization Request

Build the authorization URL. PKCE is always required for public clients and strongly recommended for confidential clients.

bash
# 1. Generate PKCE code_verifier (43-128 chars, URL-safe base64) $ VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43) $ CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=') $ STATE=$(openssl rand -hex 16)
# 2. Authorization URL — redirect the user here https://id.clavex.eu/acme/authorize ?client_id=clavex_01925... &redirect_uri=https://app.example.com/callback &response_type=code &scope=openid+email+profile &state=$STATE &code_challenge=$CHALLENGE &code_challenge_method=S256
State parameter: Always verify state matches on callback. This is your primary CSRF protection. Clavex will reject requests where state is missing.

Step 4 — Token Exchange

After the user logs in, Clavex redirects to your redirect_uri with ?code=AUTH_CODE&state=.... Exchange the code at the token endpoint.

bash
$ curl -s -X POST https://id.clavex.eu/acme/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=$AUTH_CODE" \ -d "redirect_uri=https://app.example.com/callback" \ -d "client_id=clavex_01925..." \ -d "code_verifier=$VERIFIER" | jq . { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjUt...", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjUt...", "refresh_token": "clavex_rt_01925...", "token_type": "Bearer", "expires_in": 3600, "scope": "openid email profile" }

Step 5 — UserInfo & ID Token

Fetch the user's claims from the UserInfo endpoint, or decode them directly from the ID token (a signed JWT — always verify the signature against the JWKS).

bash
# UserInfo endpoint $ curl -s https://id.clavex.eu/acme/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" | jq . { "sub": "01925f3a-...", "email": "alice@acme.eu", "email_verified": true, "name": "Alice Rossi", "locale": "it" }
# Verify ID token signature (use your library — this is just for inspection) $ echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq . { "iss": "https://id.clavex.eu/acme", "sub": "01925f3a-...", "aud": "clavex_01925...", "exp": 1746892800, "iat": 1746889200, "auth_time": 1746889190 }

Step 6 — Refresh Tokens

bash
$ curl -s -X POST https://id.clavex.eu/acme/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=$REFRESH_TOKEN" \ -d "client_id=clavex_01925..." | jq '{access_token,expires_in}'

Common Errors

ErrorCauseFix
invalid_grantCode already used, expired, or verifier wrongEach code is single-use. Check verifier matches the one sent in the auth request.
redirect_uri_mismatchredirect_uri not in client's registered listUpdate client registration or fix the URI. Must match exactly (including trailing slash).
invalid_clientWrong client_id or missing auth for confidential clientCheck client_id. Confidential clients must supply client_secret or client assertion.
login_requiredprompt=none but no active sessionRemove prompt=none or redirect the user to login.
Next: See Code Examples for ready-to-run Go, Node.js, and Python integrations using popular OIDC libraries.