Files
pangolin/server/setup/clearStaleData.ts
Severin Enzler 7176fa4a40 Revalidate OIDC sessions with the IdP using refresh tokens
Pangolin consults the IdP only at login and keeps its own session
afterwards, so disabling a user or revoking their sessions in the IdP
does not end their Pangolin sessions.

Store the IdP refresh token at OIDC login as a grant and redeem it at
most every server.idp_session_revalidation_minutes (default 5). If the
IdP rejects it with invalid_grant, the grant is marked revoked and all
sessions backed by it and their resource sessions are deleted. Other
errors (IdP unreachable or slower than 10 seconds, invalid_client after
a secret change) keep the sessions valid and are retried after at most
a minute instead of on every request.

Only the instance holding a lease on the grant in the primary database
redeems its refresh token, and it writes the result only while it still
holds the lease, so instances never redeem the same token concurrently.
The per-request lookup reads from a replica through safeRead.

Device logins of the Pangolin clients share the grant of the browser
session that approved them, read from the primary database, so a rotated
refresh token is never redeemed twice by separate sessions.

The Entra ID provider type hardcodes its scopes, so it now requests
offline_access to get a refresh token.
2026-10-29 11:33:30 +01:00

122 lines
3.2 KiB
TypeScript

import { build } from "@server/build";
import {
db,
deviceWebAuthCodes,
idpGrants,
sessionIdpGrants,
sessionTransferToken
} from "@server/db";
import {
emailVerificationCodes,
newtSessions,
passwordResetTokens,
resourceAccessToken,
resourceOtp,
resourceSessions,
sessions,
userInvites
} from "@server/db";
import logger from "@server/logger";
import { lt, notInArray } from "drizzle-orm";
export async function clearStaleData() {
try {
await db
.delete(sessions)
.where(lt(sessions.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired sessions:", e);
}
try {
await db
.delete(idpGrants)
.where(
notInArray(
idpGrants.grantId,
db
.select({ grantId: sessionIdpGrants.grantId })
.from(sessionIdpGrants)
)
);
} catch (e) {
logger.warn("Error clearing unused idpGrants:", e);
}
try {
await db
.delete(newtSessions)
.where(lt(newtSessions.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired newtSessions:", e);
}
try {
await db
.delete(emailVerificationCodes)
.where(lt(emailVerificationCodes.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired emailVerificationCodes:", e);
}
try {
await db
.delete(passwordResetTokens)
.where(lt(passwordResetTokens.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired passwordResetTokens:", e);
}
try {
await db
.delete(userInvites)
.where(lt(userInvites.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired userInvites:", e);
}
try {
await db
.delete(resourceAccessToken)
.where(lt(resourceAccessToken.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired resourceAccessToken:", e);
}
try {
await db
.delete(resourceSessions)
.where(lt(resourceSessions.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired resourceSessions:", e);
}
try {
await db
.delete(resourceOtp)
.where(lt(resourceOtp.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired resourceOtp:", e);
}
if (build !== "oss") {
try {
await db
.delete(sessionTransferToken)
.where(
lt(sessionTransferToken.expiresAt, new Date().getTime())
);
} catch (e) {
logger.warn("Error clearing expired sessionTransferToken:", e);
}
}
try {
await db
.delete(deviceWebAuthCodes)
.where(lt(deviceWebAuthCodes.expiresAt, new Date().getTime()));
} catch (e) {
logger.warn("Error clearing expired deviceWebAuthCodes:", e);
}
}