feat: implement OAuth 2.0 / OpenID Connect support
Some checks failed
Build & Push Docker Image / build (push) Failing after 1m12s
Some checks failed
Build & Push Docker Image / build (push) Failing after 1m12s
- Added OAuth configuration management in the admin panel. - Implemented OAuth authorization flow with PKCE for enhanced security. - Created routes for handling OAuth provider discovery, authorization, and callback. - Integrated OAuth login and registration options in the frontend. - Updated UI components to support OAuth login and registration. - Added internationalization strings for OAuth-related messages. - Implemented encryption for client secrets and secure state management. - Added error handling and user feedback for OAuth processes.
This commit is contained in:
@@ -5,6 +5,12 @@ import { getDb } from '../config/database.js';
|
||||
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
||||
import { isMailerConfigured, sendInviteEmail } from '../config/mailer.js';
|
||||
import { log } from '../config/logger.js';
|
||||
import {
|
||||
getOAuthConfig,
|
||||
saveOAuthConfig,
|
||||
deleteOAuthConfig,
|
||||
discoverOIDC,
|
||||
} from '../config/oauth.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{2,}$/;
|
||||
|
||||
@@ -260,4 +266,100 @@ router.delete('/invites/:id', authenticateToken, requireAdmin, async (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
// ── OAuth / SSO Configuration (admin only) ──────────────────────────────────
|
||||
|
||||
// GET /api/admin/oauth - Get current OAuth configuration
|
||||
router.get('/oauth', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const config = await getOAuthConfig();
|
||||
if (!config) {
|
||||
return res.json({ configured: false, config: null });
|
||||
}
|
||||
// Never expose the decrypted client secret to the frontend
|
||||
res.json({
|
||||
configured: true,
|
||||
config: {
|
||||
issuer: config.issuer,
|
||||
clientId: config.clientId,
|
||||
hasClientSecret: !!config.clientSecret,
|
||||
displayName: config.displayName || 'SSO',
|
||||
autoRegister: config.autoRegister ?? true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
log.admin.error(`Get OAuth config error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Could not load OAuth configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/admin/oauth - Save OAuth configuration
|
||||
router.put('/oauth', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { issuer, clientId, clientSecret, displayName, autoRegister } = req.body;
|
||||
|
||||
if (!issuer || !clientId) {
|
||||
return res.status(400).json({ error: 'Issuer URL and Client ID are required' });
|
||||
}
|
||||
|
||||
// Validate issuer URL
|
||||
try {
|
||||
const parsed = new URL(issuer);
|
||||
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
||||
return res.status(400).json({ error: 'Issuer URL must use https:// (or http:// for development)' });
|
||||
}
|
||||
} catch {
|
||||
return res.status(400).json({ error: 'Invalid Issuer URL' });
|
||||
}
|
||||
|
||||
// Validate display name length
|
||||
if (displayName && displayName.length > 50) {
|
||||
return res.status(400).json({ error: 'Display name must not exceed 50 characters' });
|
||||
}
|
||||
|
||||
// Check if the existing config has a secret and none is being sent (keep old one)
|
||||
let finalSecret = clientSecret;
|
||||
if (!clientSecret) {
|
||||
const existing = await getOAuthConfig();
|
||||
if (existing?.clientSecret) {
|
||||
finalSecret = existing.clientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt OIDC discovery to validate the issuer endpoint
|
||||
try {
|
||||
await discoverOIDC(issuer);
|
||||
} catch (discErr) {
|
||||
return res.status(400).json({
|
||||
error: `Could not discover OIDC configuration at ${issuer}: ${discErr.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
await saveOAuthConfig({
|
||||
issuer,
|
||||
clientId,
|
||||
clientSecret: finalSecret || '',
|
||||
displayName: displayName || 'SSO',
|
||||
autoRegister: autoRegister !== false,
|
||||
});
|
||||
|
||||
log.admin.info(`OAuth configuration saved by admin (issuer: ${issuer})`);
|
||||
res.json({ message: 'OAuth configuration saved' });
|
||||
} catch (err) {
|
||||
log.admin.error(`Save OAuth config error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Could not save OAuth configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/admin/oauth - Remove OAuth configuration
|
||||
router.delete('/oauth', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
await deleteOAuthConfig();
|
||||
log.admin.info('OAuth configuration removed by admin');
|
||||
res.json({ message: 'OAuth configuration removed' });
|
||||
} catch (err) {
|
||||
log.admin.error(`Delete OAuth config error: ${err.message}`);
|
||||
res.status(500).json({ error: 'Could not remove OAuth configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user