- 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>
85 lines
2.9 KiB
TypeScript
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);
|
|
}
|
|
});
|