Skip to content

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

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

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

@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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ae108d7-8817-4d7c-a777-38377db32719

📥 Commits

Reviewing files that changed from the base of the PR and between 5fc42e9 and eb07ad2.

📒 Files selected for processing (2)
  • src/client/LocalServer.ts
  • src/server/Worker.ts
💤 Files with no reviewable changes (2)
  • src/server/Worker.ts
  • src/client/LocalServer.ts

Walkthrough

Adds client and server Bearer-token authentication to singleplayer game archival: the client skips archival when not logged in, fetches a play token and posts gzip payload with Authorization; the server verifies the token and enforces that the record's player persistentID matches the authenticated identity.

Changes

Game Archival Authentication

Layer / File(s) Summary
Server-side token authentication and ownership validation
src/server/Worker.ts
The /api/archive_singleplayer_game endpoint extracts and verifies a Bearer token via verifyClientToken, sets the authenticated persistentID, and rejects requests where the submitted record's sole player persistentID does not match the token (returns 401 or 403).
Client-side authenticated archival request
src/client/LocalServer.ts
Client imports getPlayToken and isLoggedIn, skips archival when not logged in, concurrently compresses the record and fetches a play token, then POSTs gzip data to the archive endpoint with Authorization: Bearer <token> and logs non-OK responses.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LocalServer as Client LocalServer
  participant ArchiveAPI as /api/archive_singleplayer_game
  participant Worker as Worker.verifyClientToken
  participant Archive as archive()

  Client->>LocalServer: endGame()
  LocalServer->>LocalServer: isLoggedIn()
  alt not logged in
    LocalServer-->>Client: skip archival
  else logged in
    par
      LocalServer->>LocalServer: gzip(record)
      LocalServer->>LocalServer: getPlayToken()
    end
    LocalServer->>ArchiveAPI: POST gzip with Authorization: Bearer token
    ArchiveAPI->>Worker: verifyClientToken
    alt token valid
      Worker-->>ArchiveAPI: persistentID
      ArchiveAPI->>ArchiveAPI: check record.player.persistentID == persistentID
      alt matches
        ArchiveAPI->>Archive: forward record with x-api-key
        Archive-->>ArchiveAPI: success
      else mismatch
        ArchiveAPI-->>LocalServer: 403 Unauthorized user for this record
      end
    else invalid
      ArchiveAPI-->>LocalServer: 401 Unauthorized
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Celant

Poem

Tokens clap like quiet guards at night,
Compression packs the story tight.
Client checks login, sends the key,
Server asks, "Is this player thee?"
Records only pass with matching light. 🎮✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding JWT validation to secure the /api/archive_singleplayer_game endpoint.
Description check ✅ Passed The description clearly explains the security vulnerability being fixed and references the linked issue #3957.
Linked Issues check ✅ Passed The PR implements the primary requirement from issue #3957: adding verifyClientToken middleware to authenticate requests and enforce identity matching before archiving records.
Out of Scope Changes check ✅ Passed All changes directly support the security objective of authenticating the endpoint and validating user identity—no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 17, 2026
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/client/LocalServer.ts (1)

314-329: 💤 Low value

Consider logging HTTP error responses for easier debugging.

The .catch() only handles network failures. If the server returns 401, 403, or 400, the fetch promise still resolves and these errors go unlogged. With keepalive: true the page may unload before response arrives, but when it does arrive, having logs helps debug auth or validation failures.

🔧 Optional: add response status check
       Promise.all([compress(jsonString), getPlayToken()])
         .then(([compressedData, token]) => {
           return fetch(`/${workerPath}/api/archive_singleplayer_game`, {
             method: "POST",
             headers: {
               "Content-Type": "application/json",
               "Content-Encoding": "gzip",
               Authorization: `Bearer ${token}`,
             },
             body: compressedData,
             keepalive: true, // Ensures request completes even if page unloads
           });
         })
+        .then((response) => {
+          if (!response.ok) {
+            console.error(
+              `Archive request failed with status ${response.status}`,
+            );
+          }
+        })
         .catch((error) => {
           console.error("Failed to archive singleplayer game:", error);
         });
🤖 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/client/LocalServer.ts` around lines 314 - 329, The current Promise chain
(Promise.all([compress(jsonString), getPlayToken()]) ... fetch(...)) only logs
network failures, so HTTP error responses (non-2xx) from the
archive_singleplayer_game endpoint go unreported; update the then-handler that
performs fetch (after compress and getPlayToken) to inspect the fetch Response:
check response.ok and, if false, read and log the response.status,
response.statusText and response body (e.g., response.text() or json()) along
with a contextual message; keep the existing catch to handle network errors but
ensure the response check happens before finishing the chain so 400/401/403/500
errors are logged for debugging.
🤖 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/client/LocalServer.ts`:
- Around line 314-329: The current Promise chain
(Promise.all([compress(jsonString), getPlayToken()]) ... fetch(...)) only logs
network failures, so HTTP error responses (non-2xx) from the
archive_singleplayer_game endpoint go unreported; update the then-handler that
performs fetch (after compress and getPlayToken) to inspect the fetch Response:
check response.ok and, if false, read and log the response.status,
response.statusText and response body (e.g., response.text() or json()) along
with a contextual message; keep the existing catch to handle network errors but
ensure the response check happens before finishing the chain so 400/401/403/500
errors are logged for debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 305e3768-0a89-4c29-957c-f09ca1162b37

📥 Commits

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

📒 Files selected for processing (1)
  • src/client/LocalServer.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 30, 2026
@evanpelle evanpelle closed this Jun 2, 2026
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

Security Vulnerability: Unauthenticated /api/archive_singleplayer_game Endpoint

2 participants