From b0b0ebb53e39898c913f6c8b4265cdf4c52035b0 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Fri, 30 May 2025 12:10:00 -0400 Subject: [PATCH] Server role lookup (#954) ## Description: - Validate that user tokens are accepted by the API server, in case of token revoked / remote logout. - Lookup user roles by their token. - Sets the groundwork for validating custom flag codes, patterns, etc. ## 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> --- src/core/ApiSchemas.ts | 4 ---- src/server/Client.ts | 1 + src/server/Worker.ts | 18 ++++++++++++++++-- src/server/jwt.ts | 35 ++++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 1c594e1a7..112ddc8b9 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -28,10 +28,6 @@ export const TokenPayloadSchema = z.object({ iss: z.string(), aud: z.string(), exp: z.number(), - rol: z - .string() - .optional() - .transform((val) => (val ?? "").split(",")), }); export type TokenPayload = z.infer; diff --git a/src/server/Client.ts b/src/server/Client.ts index 216150dbf..6eff8b1b8 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -12,6 +12,7 @@ export class Client { public readonly clientID: ClientID, public readonly persistentID: string, public readonly claims: TokenPayload | null, + public readonly roles: string[] | null, public readonly ip: string, public readonly username: string, public readonly ws: WebSocket, diff --git a/src/server/Worker.ts b/src/server/Worker.ts index e9fb2131a..2ec20a3fd 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -19,7 +19,7 @@ import { archive, readGameRecord } from "./Archive"; import { Client } from "./Client"; import { GameManager } from "./GameManager"; import { gatekeeper, LimiterType } from "./Gatekeeper"; -import { verifyClientToken } from "./jwt"; +import { getUserMe, verifyClientToken } from "./jwt"; import { logger } from "./Logger"; import { initWorkerMetrics } from "./WorkerMetrics"; @@ -316,11 +316,25 @@ export function startWorker() { config, ); + const roles: string[] | null = null; + + // Check user roles + if (claims !== null) { + const result = await getUserMe(clientMsg.token, config); + if (result === false) { + log.warn("Token is not valid", claims); + return; + } + } + + // TODO: Validate client settings based on roles + // Create client and add to game const client = new Client( clientMsg.clientID, persistentId, - claims ?? null, + claims, + roles, ip, clientMsg.username, ws, diff --git a/src/server/jwt.ts b/src/server/jwt.ts index 150402a5f..c7896bd91 100644 --- a/src/server/jwt.ts +++ b/src/server/jwt.ts @@ -1,5 +1,10 @@ import { jwtVerify } from "jose"; -import { TokenPayload, TokenPayloadSchema } from "../core/ApiSchemas"; +import { + TokenPayload, + TokenPayloadSchema, + UserMeResponse, + UserMeResponseSchema, +} from "../core/ApiSchemas"; import { ServerConfig } from "../core/configuration/Config"; type TokenVerificationResult = { @@ -27,3 +32,31 @@ export async function verifyClientToken( const persistentId = claims.sub; return { persistentId, claims }; } + +export async function getUserMe( + token: string, + config: ServerConfig, +): Promise { + try { + // Get the user object + const response = await fetch(config.jwtIssuer() + "/users/@me", { + headers: { + authorization: `Bearer ${token}`, + }, + }); + if (response.status !== 200) return false; + const body = await response.json(); + const result = UserMeResponseSchema.safeParse(body); + if (!result.success) { + console.error( + "Invalid response", + JSON.stringify(body), + JSON.stringify(result.error), + ); + return false; + } + return result.data; + } catch (e) { + return false; + } +}