fix: harden SSO ticket flow and revoke tickets on logout
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:
1 parent
ca59a1065f
commit
7f39ba4257
7 files changed
+231
-28
No files matched your search
+26
-1
@@ -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: "/" });
|
||||
}
|
||||
Reference in new issue
Block a user