@nullplatform/np-scope-token-validator
Version:
JWT authentication service for nginx auth_request
267 lines (227 loc) • 6.83 kB
JavaScript
#!/usr/bin/env node
/**
* Simplified JWT authentication service that works
*/
const express = require('express');
const jwt = require('jsonwebtoken');
const https = require('https');
const crypto = require('crypto');
const app = express();
// Parse command line arguments
const args = process.argv.slice(2);
let PORT = 8081;
if (args.includes('--help') || args.includes('-h')) {
console.log(`
JWT Authentication Service
USAGE:
node auth-service.js [OPTIONS]
OPTIONS:
--port <number> Set the port number (default: 8081)
Must be between 1 and 65535
--help, -h Show this help message
EXAMPLES:
node auth-service.js
node auth-service.js --port 3000
node auth-service.js --port 8080
ENDPOINTS:
POST/GET /auth Main authentication endpoint
Accepts JWT tokens via:
- Cookie: np_scope_token=<token>
- Header: Authorization: Bearer <token>
GET /health Health check endpoint
Returns service status and cached keys count
GET /debug/jwks Debug JWKS fetching
Shows available keys information
AUTHENTICATION:
• JWKS URL: https://api.nullplatform.com/scope/.well-known/jwks.json
• Expected Issuer: https://api.nullplatform.com/scope
• Algorithm: RS256
• Cache TTL: 10 minutes
The service validates JWT tokens against the nullplatform JWKS endpoint
and returns success/failure with appropriate HTTP status codes.
`);
process.exit(0);
}
const portIndex = args.indexOf('--port');
if (portIndex !== -1 && args[portIndex + 1]) {
const portArg = parseInt(args[portIndex + 1], 10);
if (!isNaN(portArg) && portArg > 0 && portArg <= 65535) {
PORT = portArg;
}
}
const JWKS_URL = 'https://api.nullplatform.com/scope/.well-known/jwks.json';
const ISSUER = 'https://api.nullplatform.com/scope';
// Simple in-memory cache
let cachedKeys = null;
let cacheTime = 0;
const CACHE_TTL = 600000; // 10 minutes
/**
* Convert JWK to PEM format
*/
function jwkToPem(jwk) {
const publicKey = crypto.createPublicKey({
key: {
kty: jwk.kty,
n: jwk.n,
e: jwk.e
},
format: 'jwk'
});
return publicKey.export({
type: 'spki',
format: 'pem'
});
}
/**
* Fetch JWKS keys
*/
function fetchJwksKeys() {
return new Promise((resolve, reject) => {
// Check cache
const now = Date.now();
if (cachedKeys && (now - cacheTime) < CACHE_TTL) {
return resolve(cachedKeys);
}
console.log('Fetching fresh JWKS keys...');
https.get(JWKS_URL, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const jwks = JSON.parse(data);
const keys = jwks.keys.map(key => ({
kid: key.kid,
publicKey: jwkToPem(key),
alg: key.alg || 'RS256'
}));
// Update cache
cachedKeys = keys;
cacheTime = now;
console.log(`Successfully cached ${keys.length} JWKS keys`);
resolve(keys);
} catch (error) {
console.error('JWKS processing error:', error.message);
reject(error);
}
});
}).on('error', reject);
});
}
/**
* Extract token from request
*/
function extractToken(req) {
// Check cookie first
const cookies = req.headers.cookie || '';
const tokenMatch = cookies.match(/np_scope_token=([^;]+)/);
if (tokenMatch) {
return tokenMatch[1];
}
// Check Authorization header
const authHeader = req.headers.authorization || '';
if (authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
/**
* Main auth endpoint
*/
app.all('/auth', async (req, res) => {
try {
// Extract token
const token = extractToken(req);
if (!token) {
console.log('No token provided');
return res.status(401).json({ error: 'No token provided' });
}
// Get JWKS keys
const keys = await fetchJwksKeys();
// Decode token header
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
console.log('Failed to decode token');
return res.status(401).json({ error: 'Invalid token format' });
}
const kid = decoded.header.kid;
// Find the right key
let publicKey;
if (kid) {
const key = keys.find(k => k.kid === kid);
if (!key) {
console.log('Key with kid not found:', kid);
return res.status(401).json({ error: 'Key not found' });
}
publicKey = key.publicKey;
} else {
// Use first key if no kid specified
if (keys.length === 0) {
console.log('No keys available');
return res.status(401).json({ error: 'No keys available' });
}
publicKey = keys[0].publicKey;
}
// Verify token
const payload = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: ISSUER
});
// Success response
res.set('X-User-ID', payload.userId ? payload.userId.toString() : '');
res.set('X-User-Exp', payload.exp ? payload.exp.toString() : '');
return res.status(200).json({ status: 'valid' });
} catch (error) {
console.error('Auth error:', error.message);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token has expired' });
} else if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
} else {
return res.status(500).json({ error: 'Authentication failed' });
}
}
});
/**
* Health check
*/
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
cachedKeys: cachedKeys ? cachedKeys.length : 0
});
});
/**
* Debug JWKS endpoint
*/
app.get('/debug/jwks', async (req, res) => {
try {
const keys = await fetchJwksKeys();
res.json({
status: 'success',
keysCount: keys.length,
keys: keys.map(key => ({
kid: key.kid,
alg: key.alg
}))
});
} catch (error) {
res.status(500).json({
status: 'error',
message: error.message
});
}
});
// Start server
app.listen(PORT, '127.0.0.1', () => {
console.log(`🚀 Simple JWT Auth service running on http://127.0.0.1:${PORT}`);
console.log(`📋 Endpoints:`);
console.log(` • POST/GET /auth - Main authentication endpoint`);
console.log(` • GET /health - Health check`);
console.log(` • GET /debug/jwks - Debug JWKS fetching`);
console.log(`🔑 JWKS URL: ${JWKS_URL}`);
console.log(`🏢 Expected Issuer: ${ISSUER}`);
console.log(`Press Ctrl+C to stop the server`);
});
process.on('SIGTERM', () => process.exit(0));
process.on('SIGINT', () => process.exit(0));