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

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)

Parameter
Description
Example
Notes

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)

Parameter
Description
Value
Notes

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)

Parameter
Description
Example
Notes

code_challenge

SHA256 hash of code_verifier

"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

Base64URL encoded

code_challenge_method

Hash method

"S256"

Always "S256"

🎨 UI/UX Parameters (Optional)

Parameter
Description
Default Value
Examples

prompt

UI display behavior

"consent"

"none", "login", "consent", "select_account"

ui_locales

Interface language

"en-US"

"vi-VN", "en-US", "ja-JP"

login_hint

Email/username hint

-

max_age

Max time since last login (seconds)

3600

0 (force re-auth), 7200

🏒 Oten Specific Parameters (Optional)

Parameter
Description
Example
Notes

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 HS256

Option 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

Use Case
Recommended Parameters
Example

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