UNPKG

@nullplatform/np-scope-token-validator

Version:

JWT authentication service for nginx auth_request

267 lines (227 loc) 6.83 kB
#!/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));