Authorization Flow
Xident implements the OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange). PKCE is mandatory for all clients — public and confidential — to prevent authorization code interception attacks.
Why PKCE is mandatory
Even if you have a backend (confidential client), PKCE adds a defense-in-depth layer.
If an attacker intercepts the authorization code, they cannot exchange it for tokens without
the code_verifier. Xident follows the OAuth 2.0 Security Best Current Practice by requiring PKCE for all clients.
Step-by-step flow
Browser / App Your Backend Xident
| | |
| 1. Click "Login" | |
|------------------------→ | |
| | |
| 2. Generate PKCE pair | |
| (verifier + challenge) |
| | |
| 3. Redirect to /oauth/authorize |
| (with code_challenge) |
|--------------------------------------------------→
| | |
| | 4. User logs in |
| | 5. Consent screen |
| | 6. User approves |
| | |
| 7. Redirect to redirect_uri with ?code=xxx |
|←--------------------------------------------------
| | |
| 8. Send code + | |
| code_verifier | |
| to your backend | |
|------------------------→ | |
| | |
| | 9. POST /oauth/token |
| | (code + verifier) |
| |---------------------→ |
| | |
| | 10. Verify PKCE |
| | SHA256(verifier) |
| | == challenge? |
| | |
| | 11. Return tokens |
| |←--------------------- |
| | |
| 12. Set session | |
|←------------------------ | |
1. Generate PKCE parameters
Before redirecting the user, generate a code_verifier (a high-entropy random string)
and derive a code_challenge from it using SHA-256. Store the verifier securely —
you will need it when exchanging the authorization code for tokens.
// Generate PKCE pair (Node.js / browser)
import crypto from 'node:crypto';
// 1. Generate code_verifier: 32 random bytes -> base64url (43 chars)
const codeVerifier = crypto
.randomBytes(32)
.toString('base64url');
// e.g. "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
// 2. Derive code_challenge: SHA-256(code_verifier) -> base64url
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// e.g. "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
// Store code_verifier in session — you need it for token exchange 2. Build the authorization URL
Redirect the user's browser to the Xident authorization endpoint:
GET https://api.xident.io/oauth/authorize?
response_type=code
&client_id=xc_abc123
&redirect_uri=https://yoursite.com/callback
&scope=openid age_verification profile
&state=random_csrf_string
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 Parameters
// Required parameters
response_type = "code" // Always "code" for authorization code flow
client_id = "xc_abc123" // Your OAuth client ID
redirect_uri = "https://..." // Must exactly match a registered redirect URI
scope = "openid ..." // Space-separated list of scopes
code_challenge = "E9Mel..." // Base64url-encoded SHA-256 of code_verifier
code_challenge_method = "S256" // Always "S256" — plain is not supported
// Recommended parameters
state = "random_string" // CSRF protection — you verify this on callback | Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Your OAuth client ID (starts with xc_) |
redirect_uri | Yes | Where to redirect after authorization. Must exactly match a registered URI |
scope | Yes | Space-separated scopes. Must include openid |
code_challenge | Yes | Base64url-encoded SHA-256 hash of code_verifier |
code_challenge_method | Yes | Must be S256 |
state | Recommended | Random string for CSRF protection. Returned unchanged on callback |
3. User authenticates and consents
Xident shows the user a login screen (if not already logged in) followed by a consent screen listing the permissions your application is requesting.
// What the user sees on the consent screen:
//
// ┌──────────────────────────────────────┐
// │ Login with Xident │
// │ │
// │ "My Website" wants to: │
// │ │
// │ ✓ Verify your identity │
// │ ✓ Access your verified age bracket │
// │ ✓ Read your display name │
// │ │
// │ [Allow] [Deny] │
// └──────────────────────────────────────┘
//
// - First-time: user sees full consent screen
// - Returning: consent may be remembered (based on client settings) 4. Handle the callback
After the user approves (or denies), Xident redirects to your redirect_uri.
Successful authorization
// Successful callback redirect
https://yoursite.com/callback?
code=xac_7f3d2a1b... // Authorization code (single-use, expires in 60s)
&state=random_csrf_string // Must match what you sent Authorization denied or error
// Error callback redirect
https://yoursite.com/callback?
error=access_denied
&error_description=User+denied+the+request
&state=random_csrf_string Error codes
// Possible error codes on callback
access_denied // User clicked "Deny"
invalid_request // Missing or invalid parameter
invalid_scope // Requested scope not available
server_error // Something went wrong on Xident's side
temporarily_unavailable // Try again later Always verify the state parameter
Before processing the callback, compare the returned state to the value you stored.
If they do not match, reject the request — it may be a CSRF attack.
5. Exchange code for tokens
The authorization code is single-use and expires in 60 seconds. Exchange it for tokens immediately by sending a POST request to the token endpoint.
Request
POST https://api.xident.io/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=xc_abc123
&client_secret=xcs_secretkey
&code=xac_7f3d2a1b...
&redirect_uri=https://yoursite.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xrt_9c8b7a6e5d4c3b2a1f...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid age_verification profile"
} Node.js example
// Token exchange — Node.js example
const response = await fetch('https://api.xident.io/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.XIDENT_CLIENT_ID,
client_secret: process.env.XIDENT_CLIENT_SECRET,
code: authorizationCode,
redirect_uri: 'https://yoursite.com/callback',
code_verifier: storedCodeVerifier,
}),
});
const tokens = await response.json(); Token exchange parameters
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be authorization_code |
client_id | Yes | Your OAuth client ID |
client_secret | Yes* | Your client secret (* not required for public clients) |
code | Yes | The authorization code from the callback |
redirect_uri | Yes | Must match the URI used in the authorization request |
code_verifier | Yes | The original PKCE code verifier (before hashing) |
What happens behind the scenes
When you exchange the code, Xident:
- Validates the authorization code is unused and not expired (60s window)
- Verifies
SHA256(code_verifier) == code_challenge(the PKCE check) - Validates
client_idandclient_secret - Confirms
redirect_urimatches the one used in authorization - Issues an access token (RS256 JWT, 1 hour), refresh token, and ID token
- Invalidates the authorization code (single-use enforcement)
Next steps
- Scopes & Claims — Choose which user data to request
- Token Management — Use, refresh, and revoke tokens
- Security Best Practices — Harden your integration