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:
@@ -8,7 +8,7 @@ import type { Request, Response, NextFunction } from "express";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { db } from "../database/db/index.js";
|
||||
import { sessions, trustedDevices, apiKeys } from "../database/db/schema.js";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { eq, and, inArray, sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { DeviceType } from "./user-agent-parser.js";
|
||||
|
||||
@@ -345,6 +345,9 @@ class AuthManager {
|
||||
rememberMe?: boolean;
|
||||
deviceType?: DeviceType;
|
||||
deviceInfo?: string;
|
||||
oidcSub?: string | null;
|
||||
oidcSid?: string | null;
|
||||
ssoProviderId?: number | null;
|
||||
} = {},
|
||||
): Promise<string> {
|
||||
const jwtSecret = await this.systemCrypto.getJWTSecret();
|
||||
@@ -391,6 +394,9 @@ class AuthManager {
|
||||
jwtToken: token,
|
||||
deviceType: options.deviceType,
|
||||
deviceInfo: options.deviceInfo,
|
||||
oidcSub: options.oidcSub ?? null,
|
||||
oidcSid: options.oidcSid ?? null,
|
||||
ssoProviderId: options.ssoProviderId ?? null,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
lastActiveAt: createdAt,
|
||||
@@ -651,6 +657,62 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
async revokeSessionsByOidc(params: {
|
||||
ssoProviderId?: number | null;
|
||||
sub?: string | null;
|
||||
sid?: string | null;
|
||||
}): Promise<number> {
|
||||
const { ssoProviderId, sub, sid } = params;
|
||||
if (!sub && !sid) return 0;
|
||||
|
||||
try {
|
||||
const conditions = [
|
||||
sid ? eq(sessions.oidcSid, sid) : eq(sessions.oidcSub, sub!),
|
||||
];
|
||||
if (ssoProviderId != null)
|
||||
conditions.push(eq(sessions.ssoProviderId, ssoProviderId));
|
||||
|
||||
const matched = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(conditions.length === 1 ? conditions[0] : and(...conditions));
|
||||
|
||||
if (matched.length === 0) return 0;
|
||||
|
||||
const matchedIds = matched.map((s) => s.id);
|
||||
const affectedUsers = new Set(matched.map((s) => s.userId));
|
||||
|
||||
await db.delete(sessions).where(inArray(sessions.id, matchedIds));
|
||||
|
||||
authLogger.info("Sessions revoked via OIDC back-channel logout", {
|
||||
operation: "oidc_backchannel_logout",
|
||||
ssoProviderId,
|
||||
sessionCount: matchedIds.length,
|
||||
});
|
||||
|
||||
for (const userId of affectedUsers) {
|
||||
const remaining = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.userId, userId));
|
||||
if (remaining.length === 0) {
|
||||
this.userCrypto.logoutUser(userId);
|
||||
}
|
||||
}
|
||||
|
||||
const { saveMemoryDatabaseToFile } =
|
||||
await import("../database/db/index.js");
|
||||
await saveMemoryDatabaseToFile();
|
||||
|
||||
return matchedIds.length;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to revoke sessions via OIDC", error, {
|
||||
operation: "oidc_backchannel_logout_failed",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupExpiredSessions(): Promise<number> {
|
||||
try {
|
||||
const expiredSessions = await db
|
||||
|
||||
Reference in New Issue
Block a user