Add ODIC Back-Channel Logout (#304)

* prevent returning expired sessions

* add issuer to ODIC creds

* get id token in ODIC

* make session signin return session

* working backchannel logout?

* require https for ODIC provider

* handle wellknown not being https

* find session api progress

* fix windows build

* return session token on session

* switch OIDC to #searchSessions

* update pnpm

* switch to using message on error obj

* move odic callback

* fix type errors

* redirect old oidc callback

* make redirect url a URL

* remove scheduled task downloadCleanup

* fix session search for oidc

* fix signin result

* cleanup code

* ignore data dir

* fix lint error
This commit is contained in:
Husky
2026-01-19 17:50:04 -05:00
committed by GitHub
parent 2967e433ca
commit f04daf0388
18 changed files with 710 additions and 115 deletions
+109 -8
View File
@@ -1,16 +1,17 @@
import prisma from "../db/database";
import type { Session, SessionProvider } from "./types";
import type { SessionProvider, SessionWithToken } from "./types";
import cacheHandler from "../cache";
import type { SessionWhereInput, JsonFilter } from "~/prisma/client/models";
import type { InputJsonValue } from "@prisma/client/runtime/library";
export default function createDBSessionHandler(): SessionProvider {
const cache = cacheHandler.createCache<Session>("DBSession");
const cache = cacheHandler.createCache<SessionWithToken>("DBSession");
return {
async setSession(token, session) {
await cache.set(token, session);
await cache.set(token, { ...session, token });
// const strData = JSON.stringify(data);
await prisma.session.upsert({
const result = await prisma.session.upsert({
where: {
token,
},
@@ -28,12 +29,14 @@ export default function createDBSessionHandler(): SessionProvider {
data: session as object,
},
});
return true;
// need to cast to Session since prisma returns different json types
return result.data as unknown as SessionWithToken;
},
async updateSession(token, data) {
return await this.setSession(token, data);
return (await this.setSession(token, data)) !== undefined;
},
async getSession<T extends Session>(token: string) {
async getSession<T extends SessionWithToken>(token: string) {
const cached = await cache.get(token);
if (cached !== null) return cached as T;
@@ -44,6 +47,10 @@ export default function createDBSessionHandler(): SessionProvider {
});
if (result === null) return undefined;
// add to cache
// need to cast to Session since prisma returns a more specific type
await cache.set(token, result as SessionWithToken);
// i hate casting
// need to cast to unknown since result.data can be an N deep json object technically
// ts doesn't like that be cast down to the more constraining session type
@@ -69,5 +76,99 @@ export default function createDBSessionHandler(): SessionProvider {
},
});
},
async findSessions(options) {
const search: SessionWhereInput[] = [];
if (options.userId) {
search.push({ userId: options.userId });
}
// NOTE: in the DB, the entire session subject is stored in the "data" field
// so we need to search within that JSON object for the items we want
if (options.data && typeof options.data === "object") {
const entries = walkJsonPath(options.data);
for (const { path, value } of entries) {
const filter: JsonFilter<"Session"> = {
// set base path to data
path: ["data", ...path],
equals: value as InputJsonValue,
};
search.push({ data: filter });
}
}
if (options.oidc && typeof options.oidc === "object") {
const entries = walkJsonPath(options.oidc);
for (const { path, value } of entries) {
const filter: JsonFilter<"Session"> = {
// set base path to oidc
path: ["oidc", ...path],
equals: value as InputJsonValue,
};
search.push({ data: filter });
}
}
if (search.length === 0) {
return [];
}
// console.log("Searching sessions with:", JSON.stringify(search, null, 2));
const sessions = await prisma.session.findMany({
where: {
AND: search,
},
});
const results: SessionWithToken[] = [];
for (const session of sessions) {
// need to cast to Session since prisma returns different json types
results.push(session.data as unknown as SessionWithToken);
}
return results;
},
};
}
/**
* Walks a JSON object and returns all paths and their corresponding values.
* @param obj The JSON object to walk.
* @param basePath The base path to start from (used for recursion).
* @returns An array of objects containing the path and value.
*/
function walkJsonPath(
obj: unknown,
basePath: string[] = [],
): Array<{ path: string[]; value: unknown }> {
const results: Array<{ path: string[]; value: unknown }> = [];
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const v = obj[i];
if (v === undefined) continue;
if (v !== null && typeof v === "object") {
results.push(...walkJsonPath(v, [...basePath, String(i)]));
} else {
results.push({ path: [...basePath, String(i)], value: v });
}
}
return results;
}
if (obj !== null && typeof obj === "object") {
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
if (v === undefined) continue;
if (v !== null && typeof v === "object") {
results.push(...walkJsonPath(v, [...basePath, k]));
} else {
results.push({ path: [...basePath, k], value: v });
}
}
return results;
}
if (basePath.length > 0) {
results.push({ path: basePath, value: obj });
}
return results;
}