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>
This commit is contained in:
Scott Anderson
2025-05-30 12:10:00 -04:00
committed by evanpelle
parent de089291f4
commit b0b0ebb53e
4 changed files with 51 additions and 7 deletions
-4
View File
@@ -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<typeof TokenPayloadSchema>;
+1
View File
@@ -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,
+16 -2
View File
@@ -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,
+34 -1
View File
@@ -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<UserMeResponse | false> {
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;
}
}