145 lines
3.9 KiB
JavaScript
145 lines
3.9 KiB
JavaScript
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; |