Better handling of bad tokens (#1180)

## Description:

- Better handling of bad tokens
- Send a code and reason when closing the websocket more consistently.

## 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
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Scott Anderson
2025-06-16 21:45:30 -04:00
committed by GitHub
parent c8f04d08e0
commit b17b925b3b
4 changed files with 41 additions and 28 deletions
+1 -1
View File
@@ -144,7 +144,7 @@ const SafeString = z
)
.max(1000);
const PersistentIdSchema = z.string().uuid();
export const PersistentIdSchema = z.string().uuid();
const JwtTokenSchema = z.string().jwt();
const TokenSchema = z
.string()
+2 -2
View File
@@ -193,7 +193,7 @@ export class GameServer {
this.log.error("Failed to parse client message", error, {
clientID: client.clientID,
});
client.ws.close();
client.ws.close(1002, "ClientMessageSchema");
return;
}
const clientMsg = parsed.data;
@@ -253,7 +253,7 @@ export class GameServer {
client.ws.removeAllListeners("error");
client.ws.on("error", (error: Error) => {
if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") {
client.ws.close(1002);
client.ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1");
}
});
+14 -8
View File
@@ -300,7 +300,7 @@ export function startWorker() {
if (!parsed.success) {
const error = z.prettifyError(parsed.error);
log.warn("Error parsing join message client", error);
ws.close();
ws.close(1002, "ClientJoinMessageSchema");
return;
}
const clientMsg = parsed.data;
@@ -315,18 +315,24 @@ export function startWorker() {
return;
}
const { persistentId, claims } = await verifyClientToken(
clientMsg.token,
config,
);
const result = await verifyClientToken(clientMsg.token, config);
if (result === false) {
log.warn("Failed to verify token");
ws.close(1002, "Failed to verify token");
return;
}
const { persistentId, claims } = result;
let roles: string[] | undefined;
// Check user roles
if (claims !== null) {
if (claims === null) {
// TODO: Verify that the persistendId is is not a registered player
} else {
// Verify token and get player permissions
const result = await getUserMe(clientMsg.token, config);
if (result === false) {
log.warn("Token is not valid", claims);
ws.close(1002, "Token is not valid");
return;
}
roles = result.player.roles;
@@ -374,7 +380,7 @@ export function startWorker() {
ws.on("error", (error: Error) => {
if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") {
ws.close(1002);
ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1");
}
});
});
+24 -17
View File
@@ -6,31 +6,38 @@ import {
UserMeResponseSchema,
} from "../core/ApiSchemas";
import { ServerConfig } from "../core/configuration/Config";
import { PersistentIdSchema } from "../core/Schemas";
type TokenVerificationResult = {
persistentId: string;
claims: TokenPayload | null;
};
type TokenVerificationResult =
| {
persistentId: string;
claims: TokenPayload | null;
}
| false;
export async function verifyClientToken(
token: string,
config: ServerConfig,
): Promise<TokenVerificationResult> {
if (token.length === 36) {
if (PersistentIdSchema.safeParse(token).success) {
return { persistentId: token, claims: null };
}
const issuer = config.jwtIssuer();
const audience = config.jwtAudience();
const key = await config.jwkPublicKey();
const { payload, protectedHeader } = await jwtVerify(token, key, {
algorithms: ["EdDSA"],
issuer,
audience,
maxTokenAge: "6 days",
});
const claims = TokenPayloadSchema.parse(payload);
const persistentId = claims.sub;
return { persistentId, claims };
try {
const issuer = config.jwtIssuer();
const audience = config.jwtAudience();
const key = await config.jwkPublicKey();
const { payload, protectedHeader } = await jwtVerify(token, key, {
algorithms: ["EdDSA"],
issuer,
audience,
maxTokenAge: "6 days",
});
const claims = TokenPayloadSchema.parse(payload);
const persistentId = claims.sub;
return { persistentId, claims };
} catch (e) {
return false;
}
}
export async function getUserMe(