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:

  1. Validates the authorization code is unused and not expired (60s window)
  2. Verifies SHA256(code_verifier) == code_challenge (the PKCE check)
  3. Validates client_id and client_secret
  4. Confirms redirect_uri matches the one used in authorization
  5. Issues an access token (RS256 JWT, 1 hour), refresh token, and ID token
  6. Invalidates the authorization code (single-use enforcement)

Next steps