Copy // package.json dependencies:
// {
// "jsonwebtoken": "^9.0.0",
// "express": "^4.18.0",
// "crypto": "^1.0.1"
// }
const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
// Configuration
const config = {
clientId: process.env.OTEN_CLIENT_ID,
clientSecret: process.env.OTEN_CLIENT_SECRET,
redirectUri: process.env.OTEN_REDIRECT_URI,
privateKeyPath: process.env.OTEN_PRIVATE_KEY_PATH,
keyId: process.env.OTEN_KEY_ID,
environment: process.env.OTEN_ENV || 'production'
};
const baseUrl = config.environment === 'production'
? 'https://account.oten.com'
: 'https://account.sbx.oten.dev';
// PKCE helper functions
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
async function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256').update(verifier).digest();
return hash.toString('base64url');
}
// JAR creation functions
async function createJARWithHS256(authParams) {
const now = Math.floor(Date.now() / 1000);
const jarPayload = {
// Required JWT claims
iss: authParams.client_id,
aud: baseUrl,
iat: now,
exp: now + 300, // 5 minutes
jti: crypto.randomUUID(),
// OAuth parameters
...authParams
};
return jwt.sign(jarPayload, config.clientSecret, {
algorithm: 'HS256',
header: { alg: 'HS256', typ: 'JWT' }
});
}
async function createJARWithEdDSA(authParams) {
const now = Math.floor(Date.now() / 1000);
const privateKey = fs.readFileSync(config.privateKeyPath, 'utf8');
const jarPayload = {
// Required JWT claims
iss: authParams.client_id,
aud: baseUrl,
iat: now,
exp: now + 300, // 5 minutes
jti: crypto.randomUUID(),
// OAuth parameters
...authParams
};
return jwt.sign(jarPayload, privateKey, {
algorithm: 'EdDSA',
header: { alg: 'EdDSA', typ: 'JWT', kid: config.keyId }
});
}
// Routes
app.get('/login', async (req, res) => {
try {
// Generate PKCE parameters
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = crypto.randomUUID();
// Store in session (use proper session storage in production)
req.session = { codeVerifier, state };
// Create OAuth parameters
const authParams = {
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: 'code',
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};
// Create JAR
const requestJWT = config.clientSecret
? await createJARWithHS256(authParams)
: await createJARWithEdDSA(authParams);
// Redirect to authorization endpoint
const authURL = `${baseUrl}/v1/oauth/authorize?client_id=${config.clientId}&request=${requestJWT}`;
res.redirect(authURL);
} catch (error) {
console.error('Login error:', error);
res.status(500).send('Login failed');
}
});
app.get('/callback', async (req, res) => {
try {
const { code, state, error } = req.query;
if (error) {
return res.status(400).send(`Authorization error: ${error}`);
}
// Verify state
if (state !== req.session.state) {
return res.status(400).send('Invalid state parameter');
}
// Exchange code for tokens
const tokenResponse = await fetch(`${baseUrl}/v1/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: req.session.codeVerifier,
...(config.clientSecret && { client_secret: config.clientSecret })
})
});
const tokens = await tokenResponse.json();
if (tokens.status === 'OK') {
const tokenData = tokens.data[0];
// Store tokens securely (use proper storage in production)
req.session.tokens = tokenData;
// Decode ID token for user info
const idTokenPayload = JSON.parse(Buffer.from(tokenData.id_token.split('.')[1], 'base64').toString());
res.json({
message: 'Login successful',
user: {
id: idTokenPayload.sub,
email: idTokenPayload.email,
name: idTokenPayload.name
},
tokens: {
access_token: tokenData.access_token,
expires_in: tokenData.expires_in
}
});
} else {
res.status(400).json({ error: 'Token exchange failed', details: tokens });
}
} catch (error) {
console.error('Callback error:', error);
res.status(500).send('Callback processing failed');
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('Visit http://localhost:3000/login to start OAuth flow');
});