Files
portal/server/src/routes/auth.ts
T
Kerim AtilganandClaude Opus 5.5 edc64f159a Abmelden beendet auch die Authentik-Sitzung und springt zurück ins Tool
- Kopfleiste meldet immer über das Portal ab, auch ohne Portal-Sitzung,
  und gibt die aktuelle Seite als returnTo mit.
- Portal merkt sich den Rücksprung über den Umweg zu Authentik.
- Setup-Skript setzt den Abmelde-Ablauf des Providers 'Portal' auf
  default-invalidation-flow (der Provider-Ablauf meldet nur bei der App ab).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 09:55:42 +01:00

85 lines
2.9 KiB
TypeScript

import { Router } from 'express';
import { config, isAllowedUrl } from '../config.js';
import { buildLoginUrl, buildLogoutUrl, handleCallback } from '../oidc.js';
export const authRouter = Router();
/** Rücksprung nur auf das Portal selbst oder erlaubte Tool-Domains. */
function safeReturnTo(raw: unknown): string {
if (typeof raw !== 'string' || !raw) return '/';
if (raw.startsWith('/') && !raw.startsWith('//')) return raw;
return isAllowedUrl(raw) ? raw : '/';
}
authRouter.get('/login', async (req, res, next) => {
try {
const returnTo = safeReturnTo(req.query.returnTo);
if (config.devUser) {
req.session.user = {
sub: `dev-${config.devUser}`,
username: config.devUser,
name: config.devUser.charAt(0).toUpperCase() + config.devUser.slice(1),
email: `${config.devUser}@example.invalid`,
groups: ['HI-Mitarbeiter', 'HI-Admin'],
};
return res.redirect(returnTo);
}
const { url, state, codeVerifier } = await buildLoginUrl();
req.session.login = { state, codeVerifier, returnTo };
req.session.save((err) => (err ? next(err) : res.redirect(url)));
} catch (err) {
next(err);
}
});
authRouter.get('/callback', async (req, res) => {
const pending = req.session.login;
if (!pending) return res.redirect('/auth/login');
try {
const query = req.originalUrl.slice(req.originalUrl.indexOf('?'));
const { user, idToken } = await handleCallback(query, pending);
const returnTo = pending.returnTo;
// Neue Session-ID nach dem Login (Schutz vor Session Fixation).
req.session.regenerate((err) => {
if (err) return res.status(500).send('Anmeldung fehlgeschlagen.');
req.session.user = user;
req.session.idToken = idToken;
req.session.save(() => res.redirect(returnTo));
});
} catch (err) {
console.error('[auth] Callback fehlgeschlagen:', err);
delete req.session.login;
res.status(401).send('Anmeldung fehlgeschlagen. <a href="/auth/login">Erneut versuchen</a>');
}
});
/** Merkt sich über den Umweg zu Authentik, wohin es nach dem Abmelden zurückgeht (siehe index.ts). */
export const AFTER_LOGOUT_COOKIE = 'hi.portal.nach_abmelden';
export function safeAfterLogout(raw: unknown): string | null {
const target = safeReturnTo(raw);
return target === '/' ? null : target;
}
// Beendet Portal- und Authentik-Sitzung, auch wenn die Portal-Sitzung schon abgelaufen ist.
authRouter.get('/logout', async (req, res, next) => {
try {
const url = await buildLogoutUrl(req.session.idToken);
const returnTo = safeAfterLogout(req.query.returnTo);
req.session.destroy(() => {
res.clearCookie('hi.portal.sid');
if (returnTo) {
res.cookie(AFTER_LOGOUT_COOKIE, returnTo, {
httpOnly: true,
sameSite: 'lax',
secure: config.isProduction,
maxAge: 5 * 60 * 1000,
});
}
res.redirect(url);
});
} catch (err) {
next(err);
}
});