mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: implement OIDC back-channel logout support with session management (#1028)
* feat: implement OIDC back-channel logout support with session management * Fix OIDC back-channel logout handling * Require logout token replay identifiers --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
||||
extractOidcGroups,
|
||||
loadProviderConfig,
|
||||
buildFetchOptions,
|
||||
resolveProviderByIssuer,
|
||||
validateLogoutToken,
|
||||
} from "./user-oidc-utils.js";
|
||||
import { registerUserApiKeyRoutes } from "./user-api-key-routes.js";
|
||||
import { registerUserSettingsRoutes } from "./user-settings-routes.js";
|
||||
@@ -1464,10 +1466,16 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
"oidc_role_shared_credentials",
|
||||
);
|
||||
|
||||
const oidcSub = typeof userInfo.sub === "string" ? userInfo.sub : null;
|
||||
const oidcSid = typeof userInfo.sid === "string" ? userInfo.sid : null;
|
||||
|
||||
const token = await authManager.generateJWTToken(userRecord.id, {
|
||||
deviceType: deviceInfo.type,
|
||||
deviceInfo: deviceInfo.deviceInfo,
|
||||
rememberMe: storedRememberMe,
|
||||
oidcSub,
|
||||
oidcSid,
|
||||
ssoProviderId: callbackProviderDbId ?? null,
|
||||
});
|
||||
|
||||
authLogger.success("OIDC login successful", {
|
||||
@@ -1788,6 +1796,84 @@ router.post("/logout", authenticateJWT, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const seenLogoutJti = new Map<string, number>();
|
||||
const LOGOUT_JTI_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
function pruneLogoutJti(now: number): void {
|
||||
for (const [key, expiry] of seenLogoutJti) {
|
||||
if (expiry <= now) seenLogoutJti.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function isReplayedJti(jti: string): boolean {
|
||||
const now = Date.now();
|
||||
pruneLogoutJti(now);
|
||||
return seenLogoutJti.has(jti);
|
||||
}
|
||||
|
||||
function markLogoutJti(jti: string): void {
|
||||
const now = Date.now();
|
||||
pruneLogoutJti(now);
|
||||
seenLogoutJti.set(jti, now + LOGOUT_JTI_TTL_MS);
|
||||
}
|
||||
|
||||
router.post("/oidc/backchannel-logout", async (req, res) => {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
|
||||
try {
|
||||
const logoutToken = (req.body as Record<string, unknown> | undefined)
|
||||
?.logout_token;
|
||||
if (typeof logoutToken !== "string" || !logoutToken) {
|
||||
return res.status(400).json({ error: "missing logout_token" });
|
||||
}
|
||||
|
||||
let issuer: string | null = null;
|
||||
try {
|
||||
const parts = logoutToken.split(".");
|
||||
if (parts.length === 3) {
|
||||
const claims = JSON.parse(Buffer.from(parts[1], "base64").toString());
|
||||
issuer = typeof claims.iss === "string" ? claims.iss : null;
|
||||
}
|
||||
} catch {
|
||||
issuer = null;
|
||||
}
|
||||
|
||||
if (!issuer) {
|
||||
return res.status(400).json({ error: "invalid logout_token" });
|
||||
}
|
||||
|
||||
const provider = await resolveProviderByIssuer(issuer);
|
||||
if (!provider) {
|
||||
authLogger.warn("Back-channel logout for unknown issuer", { issuer });
|
||||
return res.status(400).json({ error: "unknown issuer" });
|
||||
}
|
||||
|
||||
const claims = await validateLogoutToken(logoutToken, provider.config);
|
||||
|
||||
if (claims.jti && isReplayedJti(claims.jti)) {
|
||||
return res.status(200).json({ ok: true });
|
||||
}
|
||||
|
||||
try {
|
||||
await authManager.revokeSessionsByOidc({
|
||||
ssoProviderId: provider.providerDbId,
|
||||
sub: claims.sub,
|
||||
sid: claims.sid,
|
||||
});
|
||||
} catch (err) {
|
||||
authLogger.error("OIDC back-channel session revocation failed", err);
|
||||
return res.status(500).json({ error: "logout processing failed" });
|
||||
}
|
||||
|
||||
markLogoutJti(claims.jti);
|
||||
|
||||
return res.status(200).json({ ok: true });
|
||||
} catch (err) {
|
||||
authLogger.error("OIDC back-channel logout failed", err);
|
||||
return res.status(400).json({ error: "invalid logout_token" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /users/me:
|
||||
|
||||
Reference in New Issue
Block a user