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 support@oten.dev 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
-
"user@example.com"
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: "user@company.com", // 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
};
// Auto-select workspace for user:
const jarPayload = {
// ... other parameters
workspace_hint: "workspace-abc123", // User will be redirected to this workspace
login_hint: "user@company.com" // 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:
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
}]
};
}
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;
}
// โ 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
};