* fix: resolve OIDC session loss and migration failures This commit fixes three critical issues affecting OIDC/SSO authentication: 1. Session Not Saved Before Redirect - Added explicit req.session.save() callback in OIDC callback handler - Ensures session is persisted before redirecting to /today - Prevents 401 errors after successful SSO authentication 2. Migration Resilience - Added DROP TABLE IF EXISTS users_new in migration - Prevents "table already exists" errors from failed migrations - Created cleanup script for orphaned migration tables 3. Trust Proxy Documentation - Documented TUDUDI_TRUST_PROXY requirement for reverse proxy deployments - Added troubleshooting guide for session loss issues - Updated .env.example with OIDC configuration examples Fixes session loss when deployed behind reverse proxies (nginx, Traefik, etc.) Changes: - backend/modules/oidc/controller.js: Add session.save() before redirect - backend/migrations/20260420000004-make-password-optional.js: Add DROP TABLE IF EXISTS - backend/scripts/cleanup-failed-migration.js: New cleanup utility - backend/.env.example: Add OIDC and trust proxy examples - docs/10-oidc-sso.md: Add trust proxy configuration and troubleshooting - docs/feature-plans/00-oidc-sso.md: Document required environment variables * fix: prettier formatting in cleanup script
209 lines
6 KiB
JavaScript
209 lines
6 KiB
JavaScript
const oidcService = require('./service');
|
|
const provisioningService = require('./provisioningService');
|
|
const oidcIdentityService = require('./oidcIdentityService');
|
|
const providerConfig = require('./providerConfig');
|
|
const auditService = require('./auditService');
|
|
|
|
async function listProviders(req, res) {
|
|
try {
|
|
const providers = providerConfig.getAllProviders();
|
|
|
|
const publicProviders = providers.map((p) => ({
|
|
slug: p.slug,
|
|
name: p.name,
|
|
type: 'oidc',
|
|
}));
|
|
|
|
res.json({ providers: publicProviders });
|
|
} catch (error) {
|
|
console.error('Error listing OIDC providers:', error);
|
|
res.status(500).json({ error: 'Failed to list providers' });
|
|
}
|
|
}
|
|
|
|
async function initiateAuth(req, res) {
|
|
try {
|
|
const { slug } = req.params;
|
|
|
|
const { authUrl } = await oidcService.initiateAuthFlow(slug, false);
|
|
|
|
res.redirect(authUrl);
|
|
} catch (error) {
|
|
console.error('Error initiating OIDC auth:', error);
|
|
|
|
const message = error.message || 'Failed to initiate authentication';
|
|
res.redirect(`/login?error=${encodeURIComponent(message)}`);
|
|
}
|
|
}
|
|
|
|
async function handleCallback(req, res) {
|
|
try {
|
|
const { slug } = req.params;
|
|
|
|
const result = await oidcService.handleCallback(slug, req.query);
|
|
|
|
if (result.linkMode) {
|
|
if (!req.currentUser) {
|
|
return res.redirect(
|
|
'/login?error=' +
|
|
encodeURIComponent(
|
|
'Authentication required to link account'
|
|
)
|
|
);
|
|
}
|
|
|
|
await provisioningService.linkIdentityToUser(
|
|
req.currentUser.id,
|
|
slug,
|
|
result.claims
|
|
);
|
|
|
|
await auditService.logOidcLinked(req.currentUser.id, slug, req);
|
|
|
|
return res.redirect('/profile/security?success=linked');
|
|
}
|
|
|
|
const { user, isNewUser } = await provisioningService.provisionUser(
|
|
slug,
|
|
result.claims,
|
|
req
|
|
);
|
|
|
|
req.session.userId = user.id;
|
|
|
|
await auditService.logOidcProvision(user.id, slug, req, isNewUser);
|
|
await auditService.logLoginSuccess(
|
|
user.id,
|
|
auditService.AUTH_METHODS.OIDC,
|
|
req,
|
|
slug
|
|
);
|
|
|
|
req.session.save((err) => {
|
|
if (err) {
|
|
console.error('Error saving session after OIDC login:', err);
|
|
return res.redirect(
|
|
'/login?error=' +
|
|
encodeURIComponent('Failed to establish session')
|
|
);
|
|
}
|
|
res.redirect('/today');
|
|
});
|
|
} catch (error) {
|
|
console.error('Error handling OIDC callback:', error);
|
|
|
|
await auditService.logLoginFailed(
|
|
null,
|
|
auditService.AUTH_METHODS.OIDC,
|
|
req,
|
|
req.params.slug,
|
|
error.message
|
|
);
|
|
|
|
const message = error.message || 'Authentication failed';
|
|
res.redirect(`/login?error=${encodeURIComponent(message)}`);
|
|
}
|
|
}
|
|
|
|
async function initiateLink(req, res) {
|
|
try {
|
|
if (!req.currentUser) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const { slug } = req.params;
|
|
|
|
const { authUrl } = await oidcService.initiateAuthFlow(slug, true);
|
|
|
|
res.json({ redirectUrl: authUrl });
|
|
} catch (error) {
|
|
console.error('Error initiating OIDC link:', error);
|
|
res.status(500).json({
|
|
error: error.message || 'Failed to initiate linking',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function unlinkIdentity(req, res) {
|
|
try {
|
|
if (!req.currentUser) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const { identityId } = req.params;
|
|
|
|
const canUnlink = await oidcIdentityService.canUnlink(
|
|
identityId,
|
|
req.currentUser.id
|
|
);
|
|
|
|
if (!canUnlink.canUnlink) {
|
|
return res.status(400).json({ error: canUnlink.reason });
|
|
}
|
|
|
|
const identity = await oidcIdentityService.getIdentityById(identityId);
|
|
|
|
await oidcIdentityService.unlinkIdentity(
|
|
identityId,
|
|
req.currentUser.id
|
|
);
|
|
|
|
await auditService.logOidcUnlinked(
|
|
req.currentUser.id,
|
|
identity.provider_slug,
|
|
req
|
|
);
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Error unlinking OIDC identity:', error);
|
|
res.status(500).json({
|
|
error: error.message || 'Failed to unlink identity',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function getUserIdentities(req, res) {
|
|
try {
|
|
if (!req.currentUser) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const identities = await oidcIdentityService.getUserIdentities(
|
|
req.currentUser.id
|
|
);
|
|
|
|
const providersMap = {};
|
|
providerConfig.getAllProviders().forEach((p) => {
|
|
providersMap[p.slug] = p;
|
|
});
|
|
|
|
const enrichedIdentities = identities.map((identity) => ({
|
|
id: identity.id,
|
|
provider_slug: identity.provider_slug,
|
|
provider_name:
|
|
providersMap[identity.provider_slug]?.name ||
|
|
identity.provider_slug,
|
|
email: identity.email,
|
|
name: identity.name,
|
|
picture: identity.picture,
|
|
first_login_at: identity.first_login_at,
|
|
last_login_at: identity.last_login_at,
|
|
created_at: identity.created_at,
|
|
}));
|
|
|
|
res.json({ identities: enrichedIdentities });
|
|
} catch (error) {
|
|
console.error('Error fetching OIDC identities:', error);
|
|
res.status(500).json({ error: 'Failed to fetch identities' });
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
listProviders,
|
|
initiateAuth,
|
|
handleCallback,
|
|
initiateLink,
|
|
unlinkIdentity,
|
|
getUserIdentities,
|
|
};
|