Skip to content

fix(api): secure /api/archive_singleplayer_game endpoint with JWT validation#3954

Open
berkelmali wants to merge 3 commits into
openfrontio:mainfrom
berkelmali:fix-api-auth
Open

fix(api): secure /api/archive_singleplayer_game endpoint with JWT validation#3954
berkelmali wants to merge 3 commits into
openfrontio:mainfrom
berkelmali:fix-api-auth

Conversation

@berkelmali
Copy link
Copy Markdown
Contributor

@berkelmali berkelmali commented May 17, 2026

Resolves #3957

Description:

This PR patches a critical unauthenticated API endpoint at /api/archive_singleplayer_game in Worker.ts. The endpoint was accepting game records directly from the request body without any authentication and forwarding them to the central database, enabling anyone to spoof and pollute singleplayer statistics.

A verifyClientToken authentication middleware check has been added to the endpoint, ensuring that only authenticated requests from verified clients are processed and archived.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

barfires

game.end() is async but was called without await inside a try/catch.
The try/catch only catches synchronous errors. Added .catch() to the
returned promise to prevent unhandled promise rejections from crashing
the server.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Review Change Stack

Walkthrough

This PR introduces two focused improvements to the game server: error handling for completed games now uses promise rejection handlers instead of synchronous exceptions, and the single-player game archive endpoint now requires Bearer token authentication with user identity verification.

Changes

Game Server Updates

Layer / File(s) Summary
Game finish error handling
src/server/GameManager.ts
GameManager.tick() replaces synchronous try/catch around game.end() with asynchronous promise handling and .catch() logging, enabling proper error capture from rejected promises.
Archive endpoint authentication and authorization
src/server/Worker.ts
The /api/archive_singleplayer_game endpoint requires an Authorization header with a Bearer token, verifies it via verifyClientToken, and enforces that the record's sole player persistentID matches the authenticated user's persistentID, rejecting mismatches with 403.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • evanpelle

Poem

🎮 When games are done, errors now flow,
As promises catch what we ought to know,
And travelers archive with token in hand,
Identity matched across all the land. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly matches the main security fix: adding JWT validation to the /api/archive_singleplayer_game endpoint in Worker.ts, which is the primary change in this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly explains the security issue being fixed: an unauthenticated API endpoint accepting game records without verification. It describes the solution (adding verifyClientToken authentication) and relates directly to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/server/Worker.ts (1)

281-290: 💤 Low value

Small terminology nit: 403 error message says "Unauthorized" but should say "Forbidden".

HTTP 401 is "Unauthorized" (authentication failure), HTTP 403 is "Forbidden" (authorization failure). The message "Unauthorized user for this record" is slightly confusing when returned with status 403.

Consider using "Forbidden" or "Not allowed to archive this record" for clarity.

📝 Suggested terminology fix
       return res
         .status(403)
-        .json({ error: "Unauthorized user for this record" });
+        .json({ error: "Forbidden: cannot archive another user's record" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Worker.ts` around lines 281 - 290, The response body for the 403
branch uses wording "Unauthorized user for this record" which conflicts with the
403 semantics; update the message returned in the res.status(403).json call to
use a "Forbidden" phrasing (e.g., "Forbidden: user not allowed to archive this
record" or "Forbidden user for this record") and ensure any log.warn context
(tokenUser/recordUser) remains unchanged; locate the check around
player.persistentID !== persistentID in the function handling the request and
replace only the error string passed to res.status(403).json.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/Worker.ts`:
- Around line 281-290: The response body for the 403 branch uses wording
"Unauthorized user for this record" which conflicts with the 403 semantics;
update the message returned in the res.status(403).json call to use a
"Forbidden" phrasing (e.g., "Forbidden: user not allowed to archive this record"
or "Forbidden user for this record") and ensure any log.warn context
(tokenUser/recordUser) remains unchanged; locate the check around
player.persistentID !== persistentID in the function handling the request and
replace only the error string passed to res.status(403).json.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a65233c-b319-421e-a71a-8e772121c74c

📥 Commits

Reviewing files that changed from the base of the PR and between 2e17fb5 and 4c5660c.

📒 Files selected for processing (2)
  • src/server/GameManager.ts
  • src/server/Worker.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

Security Vulnerability: Unauthenticated /api/archive_singleplayer_game Endpoint

1 participant