const express = require('express'); const jwt = require('jsonwebtoken'); const axios = require('axios'); class CloudronOIDCMiddleware { constructor(options = {}) { this.clientId = process.env.CLOUDRON_OIDC_CLIENT_ID; this.clientSecret = process.env.CLOUDRON_OIDC_CLIENT_SECRET; this.issuer = process.env.CLOUDRON_OIDC_ISSUER; this.redirectUri = process.env.OIDC_REDIRECT_URI; this.appOrigin = process.env.CLOUDRON_APP_ORIGIN; } // Middleware to check authentication authenticate() { return async (req, res, next) => { try { // Check for existing session/token const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.authToken || req.session?.token; if (token && this.verifyToken(token)) { req.user = jwt.decode(token); return next(); } // If no valid token, redirect to OIDC login if (req.path.startsWith('/api/')) { return res.status(401).json({ error: 'Authentication required' }); } // Redirect to OIDC authorization const authUrl = this.buildAuthUrl(); res.redirect(authUrl); } catch (error) { console.error('Authentication error:', error); res.status(500).json({ error: 'Authentication failed' }); } }; } // Build OIDC authorization URL buildAuthUrl() { const params = new URLSearchParams({ response_type: 'code', client_id: this.clientId, redirect_uri: this.redirectUri, scope: 'openid profile email', state: this.generateState() }); return `${this.issuer}/auth?${params.toString()}`; } // Handle OIDC callback async handleCallback(req, res) { try { const { code, state } = req.query; if (!code) { return res.status(400).json({ error: 'Authorization code required' }); } // Exchange code for tokens const tokenResponse = await this.exchangeCodeForTokens(code); const { access_token, id_token } = tokenResponse.data; // Verify and decode the ID token const userInfo = jwt.decode(id_token); // Create user session const sessionToken = this.createSessionToken(userInfo); // Set cookie and redirect res.cookie('authToken', sessionToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days }); res.redirect('/'); } catch (error) { console.error('OIDC callback error:', error); res.status(500).json({ error: 'Authentication callback failed' }); } } // Exchange authorization code for tokens async exchangeCodeForTokens(code) { const params = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: this.redirectUri, client_id: this.clientId, client_secret: this.clientSecret }); return axios.post(`${this.issuer}/token`, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); } // Create session token createSessionToken(userInfo) { const payload = { sub: userInfo.sub, email: userInfo.email, name: userInfo.name, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days }; return jwt.sign(payload, process.env.APP_SECRET); } // Verify JWT token verifyToken(token) { try { const decoded = jwt.verify(token, process.env.APP_SECRET); return decoded.exp > Math.floor(Date.now() / 1000); } catch (error) { return false; } } // Generate random state for CSRF protection generateState() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } // Logout handler logout() { return (req, res) => { res.clearCookie('authToken'); res.redirect('/'); }; } } module.exports = CloudronOIDCMiddleware;