Initial Docmost Cloudron package
This commit is contained in:
145
oidc-middleware.js
Normal file
145
oidc-middleware.js
Normal file
@@ -0,0 +1,145 @@
|
||||
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;
|
Reference in New Issue
Block a user