fix: harden SSO ticket flow and revoke tickets on logout
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 54s

Reuse the outstanding auth_ticket instead of minting a fresh one on every
/client load, so reloading the page or opening a second tab no longer
invalidates a game session that is still connecting. New tickets are minted
with a guard against the previously-read value so concurrent launches
converge on the same ticket.

Revoke the auth_ticket when signing out (toolbar, header and sign-out
everywhere) so a leaked ticket can no longer be replayed against the
emulator, and prevent SSO leakage via referral by setting no-referrer on the
client iframe. Strip all whitespace from the ticket prefix and build the
launch URL through a tested helper that handles query strings, existing sso
params and URL fragments correctly.
This commit is contained in:
openhands committed 2026-08-29 20:54:06 +02:00
1 parent ca59a1065f
commit 7f39ba4257
7 files changed
+231 -28

No files matched your search

+26 -1
View File
@@ -9,7 +9,8 @@ import { logger } from "@/lib/logger";
/**
* Invalidate every CMS JWT for the signed-in user by bumping website_jwt_version,
* revoke personal access tokens, then end the current browser session too.
* revoke personal access tokens, revoke the game SSO ticket, then end the
* current browser session too.
*/
export async function signOutEverywhere(): Promise<void> {
const session = await auth();
@@ -24,6 +25,7 @@ export async function signOutEverywhere(): Promise<void> {
.update(User)
.set({
websiteJwtVersion: sql`${User.websiteJwtVersion} + 1`,
authTicket: "",
})
.where(eq(User.id, userId));
await invalidateJwtVersionCache(userId);
@@ -57,3 +59,26 @@ export async function signOutEverywhere(): Promise<void> {
await signOut({ redirectTo: "/login?signedOutAll=1" });
}
/**
* Log the current user out of the website AND revoke their game SSO ticket.
*
* Without revoking it, a ticket leaked via logs/history/referrers stays valid
* for the emulator after logout. Clearing the ticket makes any future client
* connection with it invalid.
*/
export async function signOutAndRevokeTicket(): Promise<void> {
const session = await auth();
const userId = Number(session?.user?.id);
if (Number.isInteger(userId) && userId > 0) {
try {
await db.update(User).set({ authTicket: "" }).where(eq(User.id, userId));
} catch (err) {
logger.warn("Failed to revoke SSO ticket during sign out", {
userId,
error: err instanceof Error ? err.message : "Unknown",
});
}
}
await signOut({ redirectTo: "/" });
}