Files
docmost-cloudron/oidc-middleware.js
2025-07-14 21:01:40 -06:00

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;