PKCE (Proof Key for Code Exchange)
PKCE (pronounced "pixy") is an extension to the Authorization Code Flow designed for clients that cannot securely store a client_secret — primarily native mobile apps and single-page applications. Instead of a shared secret, the client generates a one-time cryptographic challenge pair (code_verifier and code_challenge) for each authorization request. LoginRadius verifies this pair during the token exchange, ensuring the token request originates from the same client that initiated the authorization.
PKCE is recommended for all public clients and is required when
client_secretis not used.
How PKCE Works
- Before redirecting the user, the client generates a cryptographically random
code_verifier. - The client derives a
code_challengefrom it using SHA-256:BASE64URL(SHA256(code_verifier)). - The
code_challengeandcode_challenge_methodare sent in the authorization request. - LoginRadius stores the
code_challengetied to the issuedauthorization_code. - During token exchange, the client sends the original
code_verifierinstead of aclient_secret. - LoginRadius hashes the
code_verifierand compares it against the storedcode_challenge. If they match, the token is issued.
PKCE Flow Diagram
PKCE Step-by-Step Walkthrough
Step 1 — Generate Code Verifier & Challenge
Before redirecting the user, the client generates a cryptographically random code_verifier and derives a code_challenge from it. Both are generated fresh for every authorization request.
The code_verifier must be a cryptographically random string between 43 and 128 characters, using only unreserved URI characters (A-Z, a-z, 0-9, -, ., _, ~). The code_challenge is derived by hashing the code_verifier with SHA-256 and encoding it as a Base64URL string.
// JavaScript / Node.js
const crypto = require('crypto');
const codeVerifier = crypto.randomBytes(64).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
Step 2 — Authorization Request
The client redirects the user to the LoginRadius OIDC Authorization endpoint, including the code_challenge and code_challenge_method alongside the standard parameters.
Endpoint
GET https://{SiteURL}/service/oidc/{OIDCAppName}/authorize
SiteURL= Either<TenantName>.hub.loginradius.comor CustomDomain i.e.auth.your-app.com·OIDCAppName= your configured OIDC App name
Example Request
GET https://your-app.hub.loginradius.com/service/oidc/MyApp/authorize
?client_id=YOUR_OIDC_CLIENT_ID
&response_type=code
&scope=openid%20profile%20email
&redirect_uri=https://your-app.com/callback
&state=abc123xyz
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
| Parameter | Value | Description |
|---|---|---|
code_challenge | BASE64URL(SHA256(code_verifier)) | The derived challenge sent with the authorization request |
code_challenge_method | S256 | Hash algorithm used. S256 (SHA-256) is required. plain is supported but not recommended. |
Step 3 — Session Check
Upon receiving the request, LoginRadius checks whether the user already has an active authenticated session.
- If an active session is found → LoginRadius skips the login UI and immediately redirects to
redirect_uriwith the authorization code. - If no active session → LoginRadius renders the login UI for the user to authenticate.
Step 4 — User Authentication
The user authenticates via the login page. Two login paths are supported:
- Traditional Login — The user submits their credentials (email/password or phone).
- Social Login — The user authenticates via a configured social provider.
After successful authentication, LoginRadius stores the
code_challenge, generates a one-time, short-livedauthorization_code, and redirects back to the client'sredirect_uri.
Step 5 — Authorization Code Issuance
LoginRadius redirects to the client callback with the authorization code appended as a query parameter.
Success Response
HTTP 302
Location: https://your-app.com/callback?code={authorization_code}&state=abc123xyz
Error Response
HTTP 302
Location: https://your-app.com/callback?error=access_denied&error_description=...
Errors are returned as query parameters on the
redirect_uriby default. This behavior can be changed by passing theresponse_modeparameter in the authorization request.
Step 6 — Token Exchange
The client sends the authorization_code along with the original code_verifier to the token endpoint. The client_secret is not required and must be omitted.
Endpoint
POST https://{SiteURL}/api/oidc/{OIDCAppName}/token
The token endpoint accepts two content types:
| Content-Type | Description |
|---|---|
application/x-www-form-urlencoded | Parameters sent as a URL-encoded form body |
application/json | Parameters sent as a JSON object in the request body |
application/x-www-form-urlencoded
application/json
POST /api/oidc/{OIDCAppName}/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={authorization_code}
&client_id=YOUR_OIDC_CLIENT_ID
&code_verifier=YOUR_CODE_VERIFIER_STRING
&redirect_uri=https://your-app.com/callback
POST /api/oidc/{OIDCAppName}/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "{authorization_code}",
"client_id": "YOUR_OIDC_CLIENT_ID",
"code_verifier": "YOUR_CODE_VERIFIER_STRING",
"redirect_uri": "https://your-app.com/callback"
}
LoginRadius hashes the
code_verifierusing SHA-256 and compares it against thecode_challengestored during the authorization step. If they match, the token is issued.
Success Response
{
"access_token": "<LoginRadius JWT Access Token>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<Refresh Token>",
"id_token": "<JWT ID Token>"
}
Standard vs PKCE Flow — Key Differences
| Standard Authorization Code Flow | Authorization Code Flow with PKCE | |
|---|---|---|
| Client type | Confidential (server-side) | Public (mobile, SPA) |
| Secret used | client_secret | code_verifier / code_challenge |
| Secret storage | Stored securely on the server | Never stored — generated per request |
| Token request auth | client_secret_post or client_secret_basic | code_verifier in request body |
| Replay protection | Client secret + short-lived code | One-time code_verifier challenge pair |