Client JWT authentication (#723)

## Description:

Send JWT to the game server for verification.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [ ] 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-12 14:51:40 -04:00
committed by GitHub
parent b402a3549e
commit f8a052a6ce
16 changed files with 123 additions and 20 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
import { Colord } from "colord";
import { JWK } from "jose";
import { GameConfig, GameID } from "../Schemas";
import {
Difficulty,
@@ -29,7 +30,6 @@ export interface ServerConfig {
turnIntervalMs(): number;
gameCreationRate(): number;
lobbyMaxPlayers(map: GameMapType, mode: GameMode): number;
discordRedirectURI(): string;
numWorkers(): number;
workerIndex(gameID: GameID): number;
workerPath(gameID: GameID): string;
@@ -49,6 +49,9 @@ export interface ServerConfig {
otelUsername(): string;
otelPassword(): string;
otelEnabled(): boolean;
jwtAudience(): string;
jwtIssuer(): string;
jwkPublicKey(): Promise<JWK>;
}
export interface NukeMagnitude {
+30 -1
View File
@@ -1,3 +1,5 @@
import { JWK } from "jose";
import { z } from "zod";
import {
Difficulty,
Duos,
@@ -24,7 +26,35 @@ import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
import { pastelTheme } from "./PastelTheme";
import { pastelThemeDark } from "./PastelThemeDark";
const JwksSchema = z.object({
keys: z
.object({
alg: z.literal("EdDSA"),
crv: z.literal("Ed25519"),
kty: z.literal("OKP"),
x: z.string(),
})
.array()
.min(1),
});
export abstract class DefaultServerConfig implements ServerConfig {
private publicKey: JWK;
abstract jwtAudience(): string;
jwtIssuer(): string {
const audience = this.jwtAudience();
return audience === "localhost"
? "http://localhost:8787"
: `https://api.${audience}`;
}
async jwkPublicKey(): Promise<JWK> {
if (this.publicKey) return this.publicKey;
const jwksUrl = this.jwtIssuer() + "/.well-known/jwks.json";
const response = await fetch(jwksUrl);
const jwks = JwksSchema.parse(await response.json());
this.publicKey = jwks.keys[0];
return this.publicKey;
}
otelEnabled(): boolean {
return Boolean(
this.otelEndpoint() && this.otelUsername() && this.otelPassword(),
@@ -70,7 +100,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
}
abstract numWorkers(): number;
abstract env(): GameEnv;
abstract discordRedirectURI(): string;
turnIntervalMs(): number {
return 100;
}
+3 -3
View File
@@ -29,12 +29,12 @@ export class DevServerConfig extends DefaultServerConfig {
return 1;
}
discordRedirectURI(): string {
return "http://localhost:3000/auth/callback";
}
numWorkers(): number {
return 2;
}
jwtAudience(): string {
return "localhost";
}
gitCommit(): string {
return "DEV";
}
+3 -3
View File
@@ -5,10 +5,10 @@ export const preprodConfig = new (class extends DefaultServerConfig {
env(): GameEnv {
return GameEnv.Preprod;
}
discordRedirectURI(): string {
return "https://openfront.dev/auth/callback";
}
numWorkers(): number {
return 3;
}
jwtAudience(): string {
return "openfront.dev";
}
})();
+2 -2
View File
@@ -8,7 +8,7 @@ export const prodConfig = new (class extends DefaultServerConfig {
env(): GameEnv {
return GameEnv.Prod;
}
discordRedirectURI(): string {
return "https://openfront.io/auth/callback";
jwtAudience(): string {
return "openfront.io";
}
})();