• Documentationv1.0.0
  • Getting started
  • Tutorials
    • Authorization Code Flow
    • Authorization Code PKCE Flow
    • Client Credentials
    • Refreshing tokens
  • Rice varieties
  • Rice nutrition
  1. Home
  2. Documentation

Authorization Code Flow with PKCE

Overview

The Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the recommended OAuth 2.0 flow for public clients like Single Page Applications (SPAs) and mobile apps. It provides the security of the authorization code flow without exposing the client secret.

Why PKCE?

Traditional authorization code flow requires a client secret, which cannot be safely stored in public clients. PKCE adds an additional layer of security by:

  • Preventing authorization code interception attacks
  • Enabling secure authentication for SPAs and mobile apps
  • No client secret required

Flow Steps

1. Generate Code Verifier

Generate a random string (43-128 characters) that will be used to verify the authorization request:

function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(Buffer.from(array));
}

// Output: d29yZHMyMm1hbnl0b2tlbnRoYXRAZGlnaXRhbC1ob3N0LmNvbQ

2. Generate Code Challenge

Create a hash of the code verifier using SHA-256 and encode it:

import { createHash } from 'crypto';

function generateCodeChallenge(verifier) {
const hash = createHash('sha256').update(verifier).digest();
return base64URLEncode(hash);
}

// Output: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

3. Build Authorization URL

Construct the authorization URL with PKCE parameters:

const authUrl = new URL('https://sso-nutri.ricethailand.go.th/realms/myrealm/protocol/openid-connect/auth');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'http://localhost:3000/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomState());
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Redirect user to authUrl
window.location.href = authUrl.toString();

4. User Authorizes

The user is redirected to the authorization server and logs in.

5. Handle Callback

After successful authorization, the user is redirected back with an authorization code:

// URL: http://localhost:3000/callback?code=AUTHORIZATION_CODE&state=random_state
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');

// Verify state to prevent CSRF

6. Exchange Code for Tokens

Exchange the authorization code for tokens using the original code verifier:

async function exchangeCodeForTokens(code, codeVerifier) {
const response = await fetch('https://sso-nutri.ricethailand.go.th/realms/myrealm/protocol/openid-connect/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    client_id: 'your-client-id',
    code: code,
    redirect_uri: 'http://localhost:3000/callback',
    code_verifier: codeVerifier
  })
});

return response.json();
// Returns: { access_token, refresh_token, id_token, token_type, expires_in }
}

Complete Implementation Example

import { createHash } from 'crypto';

function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(Buffer.from(array));
}

function generateCodeChallenge(verifier) {
return base64URLEncode(
createHash('sha256').update(verifier).digest()
);
}

async function authorizeWithPKCE() {
// Step 1 & 2: Generate PKCE parameters
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// Store codeVerifier for later (sessionStorage or memory)
sessionStorage.setItem('codeVerifier', codeVerifier);

// Step 3: Build authorization URL
const authUrl = new URL('https://sso-nutri.ricethailand.go.th/realms/myrealm/protocol/openid-connect/auth');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'http://localhost:3000/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', Math.random().toString(36).substring(2));
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Redirect to authorization server
window.location.href = authUrl.toString();
}

async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const codeVerifier = sessionStorage.getItem('codeVerifier');

if (code && codeVerifier) {
// Step 6: Exchange code for tokens
const tokens = await exchangeCodeForTokens(code, codeVerifier);

  // Store tokens securely (HTTP-only cookies recommended)
  // For SPAs, use SameSite=Strict cookies set by the server
  // or in-memory storage for short-lived tokens
  document.cookie = 'access_token=' + tokens.access_token + '; SameSite=Strict; Secure';

  // Clear code verifier
  sessionStorage.removeItem('codeVerifier');

}
}

Security Best Practices

  1. Store code verifier in memory - Never store in localStorage, use sessionStorage for codeVerifier only
  2. Store tokens in HTTP-only cookies - Prefer server-set cookies over JavaScript storage
  3. Verify state parameter - Always validate the state to prevent CSRF attacks
  4. Use HTTPS - All redirects must use HTTPS in production
  5. Generate cryptographically secure random values - Use crypto.getRandomValues() or similar
  6. Use S256 challenge method - Prefer SHA-256 over plain text

When to Use PKCE

  • Single Page Applications (SPAs)
  • Mobile Applications
  • Desktop Applications
  • Any public client without a secure storage for client secrets