Step 3: Implement Authorization Flow
Now it's time to implement the OAuth 2.0 authorization flow.
IMPORTANT: Authorization requirements depend on your client type:
Confidential Clients (Server-side): JAR (JWT-Secured Authorization Request) is REQUIRED
Public Clients (SPAs/Mobile): PKCE is REQUIRED, JAR is FORBIDDEN
π Need context? Check the Integration Flow Overview to see how this step fits into the complete process.
π― What You'll Learn
In this step, you will learn:
For Confidential Clients (Server-side applications):
Create JAR (JWT-Secured Authorization Request) - REQUIRED
Sign authorization parameters in JWT format
Handle state parameter for CSRF protection
Redirect users to Oten IDP with signed requests
For Public Clients (SPAs/Mobile apps):
Implement PKCE (Proof Key for Code Exchange) - REQUIRED
Generate secure code verifier and challenge
Handle state parameter for CSRF protection
Use direct authorization parameters (NO JAR)
π Public Client (SPA/Mobile)? For comprehensive PKCE implementation with complete code examples, see the PKCE Implementation Guide or PKCE without JAR Guide.
π Authorization Flow Overview

This diagram shows the authorization flow. The specific requirements depend on your client type:
Confidential Clients: Must use JAR (JWT-Secured Authorization Request)
Public Clients: Must use PKCE with direct parameters (JAR forbidden)
π JAR (JWT-Secured Authorization Request) - For Confidential Clients
Confidential clients (server-side applications) MUST use JAR for enhanced security. Public clients MUST NOT use JAR.
Can't implement JAR? If your confidential client application cannot support JAR due to technical constraints, contact [email protected] to discuss alternative solutions.
Why JAR is Required for Confidential Clients
// β This will NOT work for confidential clients
const authURL = `https://account.oten.com/v1/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectURI}&response_type=code&scope=openid profile email&state=${state}`;
// β
Confidential clients MUST use JAR
const requestJWT = await createJAR(authParams, privateKey);
const authURL = `https://account.oten.com/v1/oauth/authorize?client_id=${clientId}&request=${requestJWT}`;
// β
Public clients MUST use direct parameters with PKCE
const authURL = `https://account.oten.com/v1/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectURI}&response_type=code&scope=openid profile email&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`;JAR Parameters Explained
JAR (JWT Authorization Request) consists of two main parts: JWT Claims and OAuth Parameters. All OAuth parameters must be included in the JWT payload instead of URL query parameters.
Standard JWT Claims (Required)
iss
Issuer - Your client ID
"your-client-id"
Must match client_id
aud
Audience - Oten IDP endpoint
"https://account.oten.com"
Fixed for Oten
iat
Issued At - JWT creation time (Unix timestamp)
1672531200
Current time
exp
Expiration - JWT expiry time (Unix timestamp)
1672531500
Max 5 minutes after iat
jti
JWT ID - Unique identifier for request
"uuid-v4-string"
Prevents replay attacks
π OAuth Parameters (Required)
client_id
Application client ID
"your-client-id"
Must match iss
redirect_uri
Callback URL after authorization
"https://yourapp.com/callback"
Must be pre-registered
response_type
Desired response type
"code"
Always "code" for Authorization Code flow
scope
Requested access permissions
"openid profile email"
Minimum requires "openid"
state
CSRF protection parameter
"random-string-32-chars"
Protects against CSRF
π PKCE Parameters (REQUIRED for Public Clients)
code_challenge
SHA256 hash of code_verifier
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Base64URL encoded
code_challenge_method
Hash method
"S256"
Always "S256"
π¨ UI/UX Parameters (Optional)
prompt
UI display behavior
"consent"
"none", "login", "consent", "select_account"
ui_locales
Interface language
"en-US"
"vi-VN", "en-US", "ja-JP"
max_age
Max time since last login (seconds)
3600
0 (force re-auth), 7200
π’ Oten Specific Parameters (Optional)
workspace_hint
Workspace ID hint
"workspace-123"
Auto-select workspace
nonce
Random value to link ID token
"random-nonce-value"
Additional security
π Complete JAR Payload Example
// JAR Payload for CONFIDENTIAL CLIENTS ONLY
const jarPayload = {
// === JWT Claims (Required) ===
iss: "your-client-id", // Issuer
aud: "https://account.oten.com", // Audience
iat: 1672531200, // Issued at (now)
exp: 1672531500, // Expires (5 minutes later)
jti: "550e8400-e29b-41d4-a716-446655440000", // Unique ID
// === OAuth Parameters (Required) ===
client_id: "your-client-id", // Client ID
redirect_uri: "https://yourapp.com/callback", // Callback URL
response_type: "code", // Authorization code flow
scope: "openid profile email", // Requested scopes
state: "abc123def456ghi789", // CSRF protection
// === PKCE Parameters (Optional for confidential clients) ===
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
code_challenge_method: "S256",
// === UI/UX Parameters (Optional) ===
prompt: "consent", // Always show consent screen
ui_locales: "en-US", // English interface
login_hint: "[email protected]", // Email hint
max_age: 3600, // Re-auth if > 1 hour
// === Oten Specific (Optional) ===
workspace_hint: "workspace-123", // Workspace hint
nonce: "random-nonce-for-security" // ID token security
};
// β οΈ PUBLIC CLIENTS: DO NOT USE JAR - Use direct parameters instead
// See PKCE without JAR guide for public client implementationπ― JAR Parameter Usage Examples
Prompt Parameter Values
// Prompt parameter values and their meanings:
const promptOptions = {
"none": "No UI shown, auto-authorize if already logged in",
"login": "Force user to login again",
"consent": "Show consent screen (default)",
};
// Usage example:
const jarPayload = {
// ... other parameters
prompt: "login", // Force re-login
max_age: 0 // Combined with max_age=0 to force re-authentication
};UI Locales Support (coming soon)
// Supported languages:
const supportedLocales = {
"en-US": "English (United States)",
"vi-VN": "TiαΊΏng Viα»t (Vietnam)",
"ar-AE": "United Arab Emirates",
};
// Multi-locale example:
const jarPayload = {
// ... other parameters
ui_locales: "vi-VN en-US", // Prefer Vietnamese, fallback to English
};Workspace Hint Usage (coming soon)
// Auto-select workspace for user:
const jarPayload = {
// ... other parameters
workspace_hint: "workspace-abc123", // User will be redirected to this workspace
login_hint: "[email protected]" // Combined with login hint (coming soon)
};Advanced Security with Nonce
// Generate nonce for ID token security:
function generateNonce() {
return crypto.randomBytes(16).toString('hex');
}
const jarPayload = {
// ... other parameters
nonce: generateNonce(), // Will be included in ID token
};
// Verify nonce in ID token after receiving callback
function verifyIDToken(idToken, expectedNonce) {
const decoded = jwt.decode(idToken);
if (decoded.nonce !== expectedNonce) {
throw new Error('Nonce mismatch - possible token replay attack');
}
}JAR Implementation (Confidential Clients Only)
Oten IDP supports two signing methods for confidential clients:
Method 1: HS256 (Client Secret) - Simpler
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
async function createJAR_HS256(authParams, clientSecret) {
const now = Math.floor(Date.now() / 1000);
const jarPayload = {
// Standard JWT claims
iss: authParams.client_id, // Issuer (your client ID)
aud: 'https://account.oten.com', // Audience (Oten)
iat: now, // Issued at
exp: now + 300, // Expires in 5 minutes
jti: crypto.randomUUID(), // Unique identifier
// OAuth parameters (ALL parameters must be in JAR)
client_id: authParams.client_id,
redirect_uri: authParams.redirect_uri,
response_type: authParams.response_type,
scope: authParams.scope,
state: authParams.state,
// PKCE parameters (if using)
code_challenge: authParams.code_challenge,
code_challenge_method: authParams.code_challenge_method,
// Additional parameters
ui_locales: 'en-US',
prompt: 'consent'
};
// Sign the JWT with client secret (HS256)
const requestJWT = jwt.sign(jarPayload, clientSecret, {
algorithm: 'HS256'
});
return requestJWT;
}Method 2: EdDSA (Ed25519 Key Pair) - More Secure
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs');
async function createJAR_EdDSA(authParams, privateKeyPath) {
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const now = Math.floor(Date.now() / 1000);
const jarPayload = {
// Standard JWT claims
iss: authParams.client_id, // Issuer (your client ID)
aud: 'https://account.oten.com', // Audience (Oten)
iat: now, // Issued at
exp: now + 300, // Expires in 5 minutes
jti: crypto.randomUUID(), // Unique identifier
// OAuth parameters (ALL parameters must be in JAR)
client_id: authParams.client_id,
redirect_uri: authParams.redirect_uri,
response_type: authParams.response_type,
scope: authParams.scope,
state: authParams.state,
// PKCE parameters (if using)
code_challenge: authParams.code_challenge,
code_challenge_method: authParams.code_challenge_method,
// Additional parameters
ui_locales: 'en-US',
prompt: 'consent'
};
// Sign the JWT with Ed25519 private key
const requestJWT = jwt.sign(jarPayload, privateKey, {
algorithm: 'EdDSA',
keyid: 'jar-key-1' // Must match your JWKS
});
return requestJWT;
}
### Complete JAR Authorization Flow
```javascript
const crypto = require('crypto');
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
function generatePKCE() {
// Generate code verifier (43-128 characters)
const codeVerifier = crypto.randomBytes(96).toString('base64url');
// Generate code challenge
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return {
codeVerifier,
codeChallenge,
codeChallengeMethod: 'S256'
};
}
// Option 1: Using HS256 (Client Secret)
async function createAuthorizationURL_HS256() {
const state = generateState();
const pkce = generatePKCE();
// Store session data
req.session.oauthState = state;
req.session.codeVerifier = pkce.codeVerifier;
const authParams = {
client_id: process.env.OTEN_CLIENT_ID,
redirect_uri: process.env.OTEN_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: pkce.codeChallenge,
code_challenge_method: pkce.codeChallengeMethod
};
// Create JAR with client secret (HS256)
const requestJWT = await createJAR_HS256(authParams, process.env.OTEN_CLIENT_SECRET);
// Create authorization URL with JAR
const authURL = new URL('https://account.oten.com/v1/oauth/authorize');
authURL.searchParams.set('client_id', authParams.client_id);
authURL.searchParams.set('request', requestJWT);
return authURL.toString();
}
// Option 2: Using EdDSA (Ed25519 Key Pair)
async function createAuthorizationURL_EdDSA() {
const state = generateState();
const pkce = generatePKCE();
// Store session data
req.session.oauthState = state;
req.session.codeVerifier = pkce.codeVerifier;
const authParams = {
client_id: process.env.OTEN_CLIENT_ID,
redirect_uri: process.env.OTEN_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: pkce.codeChallenge,
code_challenge_method: pkce.codeChallengeMethod
};
// Create JAR with Ed25519 private key
const requestJWT = await createJAR_EdDSA(authParams, process.env.JAR_PRIVATE_KEY_PATH);
// Create authorization URL with JAR
const authURL = new URL('https://account.oten.com/v1/oauth/authorize');
authURL.searchParams.set('client_id', authParams.client_id);
authURL.searchParams.set('request', requestJWT);
return authURL.toString();
}π JAR Key Management
For HS256 (Client Secret) - No Key Generation Needed
When using HS256, you use your existing client secret:
// No key generation needed - use your client secret
const clientSecret = process.env.OTEN_CLIENT_SECRET;
// Create JAR with HS256
const requestJWT = await createJAR_HS256(authParams, clientSecret);For EdDSA (Ed25519) - Key Generation Required
When using EdDSA, you need to generate Ed25519 key pairs:
const fs = require('fs');
const crypto = require('crypto');
// Generate Ed25519 key pair for JAR signing
function generateJARKeys() {
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// Save keys securely
fs.writeFileSync('jar-private-key.pem', privateKey);
fs.writeFileSync('jar-public-key.pem', publicKey);
return { publicKey, privateKey };
}
// Create JWKS for your Ed25519 public key
function createJWKS(publicKey) {
const jwk = crypto.createPublicKey(publicKey).export({
format: 'jwk'
});
return {
keys: [{
...jwk,
kid: 'jar-key-1', // Key ID
use: 'sig', // Signature use
alg: 'EdDSA' // Algorithm for Ed25519
}]
};
}π Implementation Examples
Express.js Implementation
Option 1: Using HS256 (Client Secret)
const express = require('express');
const session = require('express-session');
const jwt = require('jsonwebtoken');
const app = express();
// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 30 * 60 * 1000 // 30 minutes
}
}));
// Login route with HS256 JAR
app.get('/login', async (req, res) => {
try {
const authURL = await createAuthorizationURL_HS256();
res.redirect(authURL);
} catch (error) {
console.error('JAR login error:', error);
res.status(500).send('Login failed');
}
});
// No JWKS endpoint needed for HS256Option 2: Using EdDSA (Ed25519 Key Pair)
const express = require('express');
const session = require('express-session');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const app = express();
// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 30 * 60 * 1000 // 30 minutes
}
}));
// JWKS endpoint (REQUIRED for EdDSA)
app.get('/.well-known/jwks.json', (req, res) => {
const jwks = createJWKS();
res.json(jwks);
});
// Login route with EdDSA JAR
app.get('/login', async (req, res) => {
try {
const authURL = await createAuthorizationURL_EdDSA();
res.redirect(authURL);
} catch (error) {
console.error('JAR login error:', error);
res.status(500).send('Login failed');
}
});
function createJWKS() {
const publicKeyPem = fs.readFileSync('jar-public-key.pem', 'utf8');
const publicKey = crypto.createPublicKey(publicKeyPem);
const jwk = publicKey.export({ format: 'jwk' });
return {
keys: [{
...jwk,
kid: 'jar-key-1',
use: 'sig',
alg: 'EdDSA'
}]
};
}React SPA Implementation
Note: For SPAs, you'll need a backend service to create JAR since private keys cannot be stored in browsers.
// AuthService.js
class AuthService {
constructor() {
this.clientId = process.env.REACT_APP_CLIENT_ID;
this.redirectUri = `${window.location.origin}/callback`;
}
async login() {
try {
// Generate PKCE parameters
const pkce = await this.generatePKCE();
const state = this.generateState();
// Store in session storage
sessionStorage.setItem('code_verifier', pkce.codeVerifier);
sessionStorage.setItem('oauth_state', state);
// Call backend to create JAR
const jarResponse = await fetch('/api/create-jar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: pkce.codeChallenge,
code_challenge_method: 'S256'
})
});
const { requestJWT } = await jarResponse.json();
// Redirect to Oten with JAR
const authURL = new URL('https://account.oten.com/v1/oauth/authorize');
authURL.searchParams.set('client_id', this.clientId);
authURL.searchParams.set('request', requestJWT);
window.location.href = authURL.toString();
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
async generatePKCE() {
const codeVerifier = this.generateRandomString(128);
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
const codeChallenge = this.base64URLEncode(digest);
return { codeVerifier, codeChallenge };
}
generateState() {
return this.generateRandomString(32);
}
generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
for (let i = 0; i < length; i++) {
result += charset.charAt(Math.floor(Math.random() * charset.length));
}
return result;
}
base64URLEncode(buffer) {
const bytes = new Uint8Array(buffer);
const binary = String.fromCharCode(...bytes);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
}
// Backend endpoint to create JAR
app.post('/api/create-jar', async (req, res) => {
try {
const authParams = req.body;
const requestJWT = await createJAR(authParams, process.env.JAR_PRIVATE_KEY_PATH);
res.json({ requestJWT });
} catch (error) {
console.error('JAR creation failed:', error);
res.status(500).json({ error: 'Failed to create JAR' });
}
});Python Flask Implementation
from flask import Flask, session, redirect, url_for, request
from authlib.integrations.flask_client import OAuth
import secrets
import hashlib
import base64
app = Flask(__name__)
app.secret_key = os.environ.get('SESSION_SECRET')
oauth = OAuth(app)
oten = oauth.register(
name='oten',
client_id=os.environ.get('OTEN_CLIENT_ID'),
client_secret=os.environ.get('OTEN_CLIENT_SECRET'),
server_metadata_url='https://account.oten.com/.well-known/openid_configuration',
client_kwargs={'scope': 'openid profile email'}
)
def generate_pkce():
"""Generate PKCE code verifier and challenge"""
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(96)).decode('utf-8').rstrip('=')
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode('utf-8')).digest()
).decode('utf-8').rstrip('=')
return code_verifier, code_challenge
@app.route('/login')
def login():
# Generate PKCE parameters
code_verifier, code_challenge = generate_pkce()
# Store code verifier in session
session['code_verifier'] = code_verifier
# Create authorization URL with PKCE
redirect_uri = url_for('callback', _external=True)
return oten.authorize_redirect(
redirect_uri,
code_challenge=code_challenge,
code_challenge_method='S256'
)
@app.route('/callback')
def callback():
# Handle the callback (covered in next step)
passποΈ Advanced JAR Parameters
Custom Parameters in JAR
async function createAdvancedJAR(authParams, privateKeyPath, options = {}) {
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const now = Math.floor(Date.now() / 1000);
const jarPayload = {
// Standard JWT claims
iss: authParams.client_id,
aud: 'https://account.oten.com',
iat: now,
exp: now + 300,
jti: crypto.randomUUID(),
// Required OAuth parameters
client_id: authParams.client_id,
redirect_uri: authParams.redirect_uri,
response_type: authParams.response_type,
scope: authParams.scope,
state: authParams.state,
// PKCE parameters
code_challenge: authParams.code_challenge,
code_challenge_method: authParams.code_challenge_method,
// Advanced parameters
prompt: options.prompt || 'consent',
ui_locales: options.uiLocales || 'en-US',
login_hint: options.loginHint,
max_age: options.maxAge || 3600,
// Custom Oten parameters
workspace_hint: options.workspaceHint,
// Additional security parameters
nonce: crypto.randomBytes(16).toString('hex')
};
// Remove undefined values
Object.keys(jarPayload).forEach(key => {
if (jarPayload[key] === undefined) {
delete jarPayload[key];
}
});
return jwt.sign(jarPayload, privateKey, {
algorithm: 'RS256',
keyid: process.env.JAR_KEY_ID || 'jar-key-1'
});
}JAR with Different Options
async function createAuthorizationURL(options = {}) {
const state = generateState();
const pkce = generatePKCE();
// Store session data
req.session.oauthState = state;
req.session.codeVerifier = pkce.codeVerifier;
const authParams = {
client_id: process.env.OTEN_CLIENT_ID,
redirect_uri: process.env.OTEN_REDIRECT_URI,
response_type: 'code',
scope: options.scope || 'openid profile email',
state: state,
code_challenge: pkce.codeChallenge,
code_challenge_method: 'S256'
};
// Create JAR with custom options
const requestJWT = await createAdvancedJAR(authParams, process.env.JAR_PRIVATE_KEY_PATH, {
prompt: options.prompt,
loginHint: options.loginHint,
workspaceHint: options.workspaceHint,
uiLocales: options.uiLocales,
maxAge: options.maxAge
});
// Create authorization URL (JAR is REQUIRED)
const authURL = new URL('https://account.oten.com/v1/oauth/authorize');
authURL.searchParams.set('client_id', authParams.client_id);
authURL.searchParams.set('request', requestJWT);
return authURL.toString();
}
// Usage examples
const basicAuthURL = await createAuthorizationURL();
const forcedConsentURL = await createAuthorizationURL({ prompt: 'consent' });
const workspaceHintURL = await createAuthorizationURL({ workspaceHint: 'workspace-123' });JAR Parameter Validation
Required Parameter Validation
function validateJARParameters(authParams) {
const errors = [];
// Validate required OAuth parameters
const requiredParams = ['client_id', 'redirect_uri', 'response_type', 'scope', 'state'];
requiredParams.forEach(param => {
if (!authParams[param]) {
errors.push(`Missing required parameter: ${param}`);
}
});
// Validate response_type
if (authParams.response_type !== 'code') {
errors.push('response_type must be "code" for Authorization Code flow');
}
// Validate scope
if (authParams.scope && !authParams.scope.includes('openid')) {
errors.push('scope must include "openid" for OpenID Connect');
}
// Validate redirect_uri format
if (authParams.redirect_uri) {
try {
new URL(authParams.redirect_uri);
} catch (e) {
errors.push('redirect_uri must be a valid URL');
}
}
// Validate PKCE parameters
if (authParams.code_challenge) {
if (!authParams.code_challenge_method) {
errors.push('code_challenge_method is required when using PKCE');
} else if (authParams.code_challenge_method !== 'S256') {
errors.push('code_challenge_method must be "S256"');
}
}
// Validate state length (recommended 32+ characters)
if (authParams.state && authParams.state.length < 32) {
errors.push('state parameter should be at least 32 characters for security');
}
return errors;
}
// Usage example:
const authParams = {
client_id: process.env.OTEN_CLIENT_ID,
redirect_uri: process.env.OTEN_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
state: generateState(),
code_challenge: pkce.codeChallenge,
code_challenge_method: 'S256'
};
const validationErrors = validateJARParameters(authParams);
if (validationErrors.length > 0) {
throw new Error('JAR validation failed: ' + validationErrors.join(', '));
}JWT Claims Validation
function validateJWTClaims(jarPayload) {
const errors = [];
const now = Math.floor(Date.now() / 1000);
// Validate required JWT claims
const requiredClaims = ['iss', 'aud', 'iat', 'exp', 'jti'];
requiredClaims.forEach(claim => {
if (!jarPayload[claim]) {
errors.push(`Missing required JWT claim: ${claim}`);
}
});
// Validate issuer matches client_id
if (jarPayload.iss !== jarPayload.client_id) {
errors.push('iss (issuer) must match client_id');
}
// Validate audience
if (jarPayload.aud !== 'https://account.oten.com') {
errors.push('aud (audience) must be Oten IDP endpoint');
}
// Validate timing
if (jarPayload.iat > now + 60) {
errors.push('iat (issued at) cannot be in the future');
}
if (jarPayload.exp <= now) {
errors.push('exp (expiration) must be in the future');
}
if (jarPayload.exp > jarPayload.iat + 300) {
errors.push('exp (expiration) should not be more than 5 minutes after iat');
}
// Validate JTI format (should be UUID)
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (jarPayload.jti && !uuidRegex.test(jarPayload.jti)) {
errors.push('jti should be a valid UUID format');
}
return errors;
}π¨ Common JAR Parameter Errors
Error: "Invalid request parameter"
// β Problem: Missing required parameters in JAR
const incompleteJAR = {
iss: "client-id",
aud: "https://account.oten.com",
// Missing: iat, exp, jti, client_id, redirect_uri, etc.
};
// β
Solution: Include all required parameters
const completeJAR = {
// JWT Claims
iss: "client-id",
aud: "https://account.oten.com",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
jti: crypto.randomUUID(),
// OAuth Parameters
client_id: "client-id",
redirect_uri: "https://app.com/callback",
response_type: "code",
scope: "openid profile email",
state: "secure-random-state"
};Error: "JWT signature verification failed"
// β Problem: Wrong signing algorithm or key
const wrongAlgorithm = jwt.sign(payload, secret, { algorithm: 'RS256' }); // Not supported
// β
Solution: Use supported algorithms
const correctHS256 = jwt.sign(payload, clientSecret, { algorithm: 'HS256' });
const correctEdDSA = jwt.sign(payload, privateKey, { algorithm: 'EdDSA', keyid: 'jar-key-1' });Error: "Request JWT expired"
// β Problem: JAR expired or wrong timing
const expiredJAR = {
iat: Math.floor(Date.now() / 1000) - 600, // 10 minutes ago
exp: Math.floor(Date.now() / 1000) - 300, // 5 minutes ago (expired)
};
// β
Solution: Proper timing
const validJAR = {
iat: Math.floor(Date.now() / 1000), // Now
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes from now
};Error: "Invalid redirect_uri"
// β Problem: Redirect URI not registered or mismatched
const wrongRedirectURI = {
redirect_uri: "https://different-domain.com/callback" // Not registered
};
// β
Solution: Use exact registered URI
const correctRedirectURI = {
redirect_uri: "https://yourapp.com/callback" // Must match registration exactly
};π§ͺ Testing JAR Implementation
Test JAR Creation
async function testJARCreation() {
console.log('=== Testing JAR Creation ===');
try {
const authParams = {
client_id: process.env.OTEN_CLIENT_ID,
redirect_uri: process.env.OTEN_REDIRECT_URI,
response_type: 'code',
scope: 'openid profile email',
state: 'test-state-123',
code_challenge: 'test-challenge',
code_challenge_method: 'S256'
};
const requestJWT = await createJAR(authParams, process.env.JAR_PRIVATE_KEY_PATH);
console.log('β
JAR created successfully');
console.log('JAR length:', requestJWT.length);
// Decode JWT to verify structure (without verification)
const decoded = jwt.decode(requestJWT, { complete: true });
console.log('JAR header:', decoded.header);
console.log('JAR payload keys:', Object.keys(decoded.payload));
return requestJWT;
} catch (error) {
console.error('β JAR creation failed:', error);
throw error;
}
}Test JWKS Endpoint
async function testJWKSEndpoint() {
console.log('=== Testing JWKS Endpoint ===');
try {
const response = await fetch('https://yourapp.com/.well-known/jwks.json');
if (!response.ok) {
throw new Error(`JWKS endpoint returned ${response.status}`);
}
const jwks = await response.json();
// Validate JWKS structure
if (!jwks.keys || !Array.isArray(jwks.keys)) {
throw new Error('Invalid JWKS structure');
}
if (jwks.keys.length === 0) {
throw new Error('No keys in JWKS');
}
const key = jwks.keys[0];
const requiredFields = ['kty', 'use', 'kid', 'alg', 'n', 'e'];
const missingFields = requiredFields.filter(field => !key[field]);
if (missingFields.length > 0) {
throw new Error(`Missing JWKS fields: ${missingFields.join(', ')}`);
}
console.log('β
JWKS endpoint is valid');
console.log('Key ID:', key.kid);
console.log('Algorithm:', key.alg);
} catch (error) {
console.error('β JWKS endpoint test failed:', error);
throw error;
}
}Validate JAR Authorization URL
function validateJARAuthorizationURL(url) {
const urlObj = new URL(url);
const params = urlObj.searchParams;
// For JAR, only client_id and request parameters are required
const required = ['client_id', 'request'];
const missing = required.filter(param => !params.has(param));
if (missing.length > 0) {
throw new Error(`Missing required JAR parameters: ${missing.join(', ')}`);
}
const requestJWT = params.get('request');
// Basic JWT format validation
const jwtParts = requestJWT.split('.');
if (jwtParts.length !== 3) {
throw new Error('Invalid JWT format in request parameter');
}
try {
// Decode header and payload (without verification)
const header = JSON.parse(atob(jwtParts[0]));
const payload = JSON.parse(atob(jwtParts[1]));
// Validate header algorithm
const supportedAlgorithms = ['HS256', 'EdDSA'];
if (!supportedAlgorithms.includes(header.alg)) {
throw new Error('JAR must use HS256 or EdDSA algorithm');
}
// Kid is only required for EdDSA (not for HS256)
if (header.alg === 'EdDSA' && !header.kid) {
throw new Error('JAR header must include kid (key ID) for EdDSA');
}
// Validate payload
const requiredClaims = ['iss', 'aud', 'iat', 'exp', 'client_id', 'redirect_uri', 'response_type'];
const missingClaims = requiredClaims.filter(claim => !payload[claim]);
if (missingClaims.length > 0) {
throw new Error(`Missing JAR claims: ${missingClaims.join(', ')}`);
}
console.log('β
JAR authorization URL validation passed');
return true;
} catch (error) {
if (error.message.includes('Missing JAR claims')) {
throw error;
}
throw new Error('Failed to decode JAR: ' + error.message);
}
}Authorization Flow Checklist
Before proceeding to the callback handling, verify your implementation based on client type:
For Confidential Clients (Server-side) - JAR Required
HS256 (Client Secret)
EdDSA (Ed25519 Key Pair)
For Public Clients (SPAs/Mobile) - PKCE Required, JAR Forbidden
π JAR Parameters Best Practices
π Security Best Practices
// β
Secure JAR parameter generation
function createSecureJAR(authParams) {
const now = Math.floor(Date.now() / 1000);
return {
// JWT Claims - Always include these
iss: authParams.client_id,
aud: 'https://account.oten.com',
iat: now,
exp: now + 300, // 5 minutes max
jti: crypto.randomUUID(), // Unique for each request
// OAuth Parameters - All required
client_id: authParams.client_id,
redirect_uri: authParams.redirect_uri,
response_type: 'code',
scope: authParams.scope,
state: crypto.randomBytes(32).toString('hex'), // 32+ chars
// PKCE - Always use for public clients
code_challenge: authParams.code_challenge,
code_challenge_method: 'S256',
// Security enhancements
nonce: crypto.randomBytes(16).toString('hex'),
max_age: 3600 // Force re-auth after 1 hour
};
}π― Parameter Selection Guide
Basic Login
Standard OAuth + PKCE
scope: "openid profile email"
Force Re-auth
Add max_age=0 + prompt=login
max_age: 0, prompt: "login"
Silent Auth
prompt=none
prompt: "none" (will fail if not logged in)
Multi-language
ui_locales
ui_locales: "vi-VN en-US"
Workspace App
workspace_hint
workspace_hint: "workspace-123"
High Security
Short expiry + nonce
exp: now + 60, nonce: "random"
π Parameter Lifecycle Management
class JARParameterManager {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.activeRequests = new Map(); // Track active JAR requests
}
async createJAR(authParams, options = {}) {
// Validate parameters
const validationErrors = this.validateParameters(authParams);
if (validationErrors.length > 0) {
throw new Error(`Invalid parameters: ${validationErrors.join(', ')}`);
}
const now = Math.floor(Date.now() / 1000);
const jti = crypto.randomUUID();
const jarPayload = {
// Standard claims
iss: this.clientId,
aud: 'https://account.oten.com',
iat: now,
exp: now + (options.expirySeconds || 300),
jti: jti,
// OAuth parameters
...authParams,
// Optional parameters
...options.additionalParams
};
// Track this request
this.activeRequests.set(jti, {
created: now,
state: authParams.state,
codeVerifier: authParams.code_verifier
});
// Clean up expired requests
this.cleanupExpiredRequests();
return jwt.sign(jarPayload, this.clientSecret, { algorithm: 'HS256' });
}
validateParameters(authParams) {
const errors = [];
// Required parameters
if (!authParams.client_id) errors.push('client_id is required');
if (!authParams.redirect_uri) errors.push('redirect_uri is required');
if (!authParams.state || authParams.state.length < 32) {
errors.push('state must be at least 32 characters');
}
return errors;
}
cleanupExpiredRequests() {
const now = Math.floor(Date.now() / 1000);
for (const [jti, request] of this.activeRequests) {
if (now - request.created > 600) { // 10 minutes
this.activeRequests.delete(jti);
}
}
}
getRequestData(state) {
for (const [jti, request] of this.activeRequests) {
if (request.state === state) {
return request;
}
}
return null;
}
}JAR Parameter Monitoring
// Monitor JAR creation and usage
class JARMonitor {
constructor() {
this.metrics = {
created: 0,
expired: 0,
failed: 0,
algorithms: { HS256: 0, EdDSA: 0 }
};
}
logJARCreation(algorithm, expiryTime) {
this.metrics.created++;
this.metrics.algorithms[algorithm]++;
console.log(`JAR created: algorithm=${algorithm}, expires_in=${expiryTime}s`);
}
logJARFailure(error, parameters) {
this.metrics.failed++;
console.error('JAR creation failed:', {
error: error.message,
client_id: parameters.client_id,
scope: parameters.scope,
timestamp: new Date().toISOString()
});
}
getMetrics() {
return {
...this.metrics,
success_rate: this.metrics.created / (this.metrics.created + this.metrics.failed)
};
}
}β οΈ Common Mistakes to Avoid
For Confidential Clients (JAR)
β Don't send parameters in URL query - Oten will reject them for confidential clients
β Don't use unsupported algorithms - Only HS256 and EdDSA are supported
β Don't forget kid in JWT header for EdDSA - Must match your JWKS
β Don't make JAR expire too long - 5 minutes maximum recommended
β Don't store private keys in client-side code - Use backend for SPAs
β Don't use RSA keys - Oten only supports HS256 and EdDSA
β Don't reuse JTI values - Each JAR must have unique identifier
β Don't include sensitive data - JAR is base64 encoded, not encrypted
For Public Clients (PKCE)
β Don't use JAR - JAR is forbidden for public clients
β Don't use weak code verifiers - Must be 128 characters with sufficient entropy
β Don't store code verifier insecurely - Use sessionStorage (SPA) or Keychain/Keystore (mobile)
β Don't skip state validation - Always validate state parameter to prevent CSRF
β Don't use HTTP - HTTPS is required for all OAuth flows
β Don't include client_secret - Public clients must not use client secrets
Next: Step 4: Handle Callback
Last updated