bugfix: user receives unauthorized on reconnection (#2601)

## Description:
When rejoining, the client did not refresh the play token, so the jwt
could be expired, causing the server to reject it.

Also improved the error log when token fails

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2025-12-11 20:00:53 -08:00
committed by GitHub
parent ef0aff12e2
commit dab04eddb4
5 changed files with 26 additions and 16 deletions
-1
View File
@@ -57,7 +57,6 @@ export interface LobbyConfig {
playerName: string;
clientID: ClientID;
gameID: GameID;
token: string;
turnstileToken: string | null;
// GameStartInfo only exists when playing a singleplayer game.
gameStartInfo?: GameStartInfo;
+1 -2
View File
@@ -8,7 +8,7 @@ import { GameType } from "../core/game/Game";
import { UserSettings } from "../core/game/UserSettings";
import "./AccountModal";
import { getUserMe } from "./Api";
import { getPlayToken, userAuth } from "./Auth";
import { userAuth } from "./Auth";
import { joinLobby } from "./ClientGameRunner";
import { fetchCosmetics } from "./Cosmetics";
import "./DarkModeButton";
@@ -495,7 +495,6 @@ class Client {
},
turnstileToken: await this.getTurnstileToken(lobby),
playerName: this.usernameInput?.getCurrentUsername() ?? "",
token: await getPlayToken(),
clientID: lobby.clientID,
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
gameRecord: lobby.gameRecord,
+5 -4
View File
@@ -25,6 +25,7 @@ import {
Winner,
} from "../core/Schemas";
import { replacer } from "../core/Util";
import { getPlayToken } from "./Auth";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalServer } from "./LocalServer";
@@ -388,25 +389,25 @@ export class Transport {
}
}
joinGame() {
async joinGame() {
this.sendMsg({
type: "join",
gameID: this.lobbyConfig.gameID,
clientID: this.lobbyConfig.clientID,
token: this.lobbyConfig.token,
username: this.lobbyConfig.playerName,
cosmetics: this.lobbyConfig.cosmetics,
turnstileToken: this.lobbyConfig.turnstileToken,
token: await getPlayToken(),
} satisfies ClientJoinMessage);
}
rejoinGame(lastTurn: number) {
async rejoinGame(lastTurn: number) {
this.sendMsg({
type: "rejoin",
gameID: this.lobbyConfig.gameID,
clientID: this.lobbyConfig.clientID,
lastTurn: lastTurn,
token: this.lobbyConfig.token,
token: await getPlayToken(),
} satisfies ClientRejoinMessage);
}
+4 -2
View File
@@ -337,8 +337,10 @@ export async function startWorker() {
// Verify token signature
const result = await verifyClientToken(clientMsg.token, config);
if (result === false) {
log.warn("Unauthorized: Invalid token");
if (result.type === "error") {
log.warn(`Invalid token: ${result.message}`, {
clientID: clientMsg.clientID,
});
ws.close(1002, "Unauthorized");
return;
}
+16 -7
View File
@@ -11,17 +11,18 @@ import { PersistentIdSchema } from "../core/Schemas";
type TokenVerificationResult =
| {
type: "success";
persistentId: string;
claims: TokenPayload | null;
}
| false;
| { type: "error"; message: string };
export async function verifyClientToken(
token: string,
config: ServerConfig,
): Promise<TokenVerificationResult> {
if (PersistentIdSchema.safeParse(token).success) {
return { persistentId: token, claims: null };
return { type: "success", persistentId: token, claims: null };
}
try {
const issuer = config.jwtIssuer();
@@ -34,15 +35,23 @@ export async function verifyClientToken(
});
const result = TokenPayloadSchema.safeParse(payload);
if (!result.success) {
const error = z.prettifyError(result.error);
console.warn("Error parsing token payload", error);
return false;
return {
type: "error",
message: z.prettifyError(result.error),
};
}
const claims = result.data;
const persistentId = claims.sub;
return { persistentId, claims };
return { type: "success", persistentId, claims };
} catch (e) {
return false;
const message =
e instanceof Error
? e.message
: typeof e === "string"
? e
: "An unknown error occurred";
return { type: "error", message };
}
}