mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
Move turnstile check to api (#3845)
## Description
Re-enables Turnstile verification (was temporarily disabled in v31 to
diagnose intermittent `invalid-input-response` rejections) and moves the
verification call off the game servers.
Game servers no longer hold `TURNSTILE_SECRET_KEY` or hit
`challenges.cloudflare.com` directly. Instead they POST to
`${jwtIssuer}/turnstile` on the api-worker (authenticated with the
existing `apiKey`), which holds the secret and proxies to Cloudflare.
Shrinks blast radius and removes the secret from every game host + GH
Actions workflow.
Response from the api-worker is Zod-validated; null tokens short-circuit
to `rejected` locally.
## Please complete the following:
- [x] I have added screenshots for all UI updates (n/a — server only)
- [x] I process any text displayed to the user through translateText()
(n/a — no user-visible text)
- [ ] 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
## Discord:
evanpelle
This commit is contained in:
@@ -138,7 +138,6 @@ jobs:
|
||||
CDN_BASE: ${{ vars.CDN_BASE }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
|
||||
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
||||
|
||||
@@ -71,7 +71,6 @@ jobs:
|
||||
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
@@ -122,7 +121,6 @@ jobs:
|
||||
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
@@ -173,7 +171,6 @@ jobs:
|
||||
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
@@ -224,7 +221,6 @@ jobs:
|
||||
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
|
||||
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }}
|
||||
SSH_KEY: ~/.ssh/id_rsa
|
||||
|
||||
@@ -134,7 +134,6 @@ ENV=$ENV
|
||||
HOST=$HOST
|
||||
GHCR_IMAGE=$GHCR_IMAGE
|
||||
GHCR_TOKEN=$GHCR_TOKEN
|
||||
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
|
||||
API_KEY=$API_KEY
|
||||
DOMAIN=$DOMAIN
|
||||
SUBDOMAIN=$SUBDOMAIN
|
||||
|
||||
@@ -25,7 +25,6 @@ export enum GameEnv {
|
||||
|
||||
export interface ServerConfig {
|
||||
turnstileSiteKey(): string;
|
||||
turnstileSecretKey(): string;
|
||||
turnIntervalMs(): number;
|
||||
gameCreationRate(): number;
|
||||
numWorkers(): number;
|
||||
|
||||
@@ -43,9 +43,6 @@ const JwksSchema = z.object({
|
||||
});
|
||||
|
||||
export abstract class DefaultServerConfig implements ServerConfig {
|
||||
turnstileSecretKey(): string {
|
||||
return Env.TURNSTILE_SECRET_KEY ?? "";
|
||||
}
|
||||
abstract turnstileSiteKey(): string;
|
||||
allowedFlares(): string[] | undefined {
|
||||
return;
|
||||
|
||||
@@ -8,10 +8,6 @@ export class DevServerConfig extends DefaultServerConfig {
|
||||
return "1x00000000000000000000AA";
|
||||
}
|
||||
|
||||
turnstileSecretKey(): string {
|
||||
return "1x0000000000000000000000000000000AA";
|
||||
}
|
||||
|
||||
adminToken(): string {
|
||||
return "WARNING_DEV_ADMIN_KEY_DO_NOT_USE_IN_PRODUCTION";
|
||||
}
|
||||
|
||||
@@ -62,9 +62,6 @@ export const Env = {
|
||||
return getEnv("GAME_ENV") ?? "dev";
|
||||
},
|
||||
|
||||
get TURNSTILE_SECRET_KEY() {
|
||||
return getEnv("TURNSTILE_SECRET_KEY");
|
||||
},
|
||||
get STRIPE_PUBLISHABLE_KEY() {
|
||||
return getEnv("STRIPE_PUBLISHABLE_KEY");
|
||||
},
|
||||
|
||||
+31
-37
@@ -1,12 +1,22 @@
|
||||
import { z } from "zod";
|
||||
import { ServerConfig } from "../core/configuration/Config";
|
||||
|
||||
const TurnstileVerdictSchema = z.discriminatedUnion("status", [
|
||||
z.object({ status: z.literal("approved") }),
|
||||
z.object({ status: z.literal("rejected"), reason: z.string() }),
|
||||
]);
|
||||
|
||||
type TurnstileVerdict = z.infer<typeof TurnstileVerdictSchema>;
|
||||
|
||||
export type TurnstileResponse =
|
||||
| TurnstileVerdict
|
||||
| { status: "error"; reason: string };
|
||||
|
||||
export async function verifyTurnstileToken(
|
||||
ip: string,
|
||||
turnstileToken: string | null,
|
||||
turnstileSecret: string,
|
||||
): Promise<
|
||||
| { status: "approved" }
|
||||
| { status: "rejected"; reason: string }
|
||||
| { status: "error"; reason: string }
|
||||
> {
|
||||
config: ServerConfig,
|
||||
): Promise<TurnstileResponse> {
|
||||
if (!turnstileToken) {
|
||||
return { status: "rejected", reason: "No turnstile token provided" };
|
||||
}
|
||||
@@ -15,54 +25,38 @@ export async function verifyTurnstileToken(
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch(
|
||||
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
secret: turnstileSecret,
|
||||
response: turnstileToken,
|
||||
remoteip: ip,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
const response = await fetch(`${config.jwtIssuer()}/turnstile`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": config.apiKey(),
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({ ip, token: turnstileToken }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: "error",
|
||||
reason: `Turnstile API returned ${response.status}`,
|
||||
reason: `api-worker returned ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = (await response.json()) as {
|
||||
success: boolean;
|
||||
challenge_ts?: string;
|
||||
hostname?: string;
|
||||
"error-codes"?: string[];
|
||||
action?: string;
|
||||
cdata?: string;
|
||||
};
|
||||
|
||||
if (!result.success) {
|
||||
const codes = result["error-codes"]?.join(", ") ?? "unknown";
|
||||
const parsed = TurnstileVerdictSchema.safeParse(await response.json());
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
status: "rejected",
|
||||
reason: `Turnstile token validation failed: ${codes}`,
|
||||
status: "error",
|
||||
reason: `api-worker returned malformed response: ${parsed.error.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: "approved" };
|
||||
return parsed.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === "AbortError") {
|
||||
return {
|
||||
status: "error",
|
||||
reason: "Turnstile token validation timed out after 3 seconds",
|
||||
reason: "Turnstile token validation timed out after 5 seconds",
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -40,10 +40,6 @@ const workerId = parseInt(process.env.WORKER_ID ?? "0");
|
||||
const log = logger.child({ comp: `w_${workerId}` });
|
||||
const playlist = new MapPlaylist();
|
||||
|
||||
// TEMPORARY: Turnstile validation disabled while we diagnose intermittent
|
||||
// invalid-input-response rejections in v31. Flip back to true to re-enable.
|
||||
const TURNSTILE_ENABLED = false;
|
||||
|
||||
// Worker setup
|
||||
export async function startWorker() {
|
||||
log.info(`Worker starting...`);
|
||||
@@ -432,14 +428,11 @@ export async function startWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
// TEMPORARY: Turnstile validation disabled while we diagnose
|
||||
// intermittent invalid-input-response rejections in v31.
|
||||
// Re-enable by flipping TURNSTILE_ENABLED back to true.
|
||||
if (TURNSTILE_ENABLED && config.env() !== GameEnv.Dev) {
|
||||
if (config.env() !== GameEnv.Dev) {
|
||||
const turnstileResult = await verifyTurnstileToken(
|
||||
ip,
|
||||
clientMsg.turnstileToken,
|
||||
config.turnstileSecretKey(),
|
||||
config,
|
||||
);
|
||||
switch (turnstileResult.status) {
|
||||
case "approved":
|
||||
|
||||
@@ -7,9 +7,6 @@ export class TestServerConfig implements ServerConfig {
|
||||
turnstileSiteKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
turnstileSecretKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
apiKey(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user