Skip to content

Issue our own session tokens instead of checking PHPSESSIDs per request - #75

Merged
boomzero merged 3 commits into
masterfrom
feat/session-token
Oct 7, 2026
Merged

boomzero merged 3 commits into
masterfrom
feat/session-token

Conversation

@boomzero

@boomzero boomzero commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

What does this PR aim to accomplish?:

At the moment every request carries the user's raw xmoj PHPSESSID, and we verify it against xmoj. That causes three problems:

  1. Speed. Fetches from Cloudflare to xmoj.tech are often slow and sometimes time out. Every cache miss in CheckToken, and every notification WebSocket connect, waits on one.
  2. Daily logouts. xmoj logs users out at least once a day and the userscript logs them back in, so PHPSESSIDs keep needing fresh verification.
  3. Exposure. For legal reasons we want to see users' PHPSESSIDs as little as possible.

This PR has the backend issue its own session token. Clients send the PHPSESSID once in exchange, and after that send only the token.

How does this PR accomplish the above?:

  • Tokens (Source/SessionToken.ts, migration 0006)
    • Login takes the existing legacy auth (phpsessid cache, else profile.php) and returns a 256-bit random token. No expiry time is returned, because the lifetime slides with use.
    • D1 stores only SHA3(token) → user_id in session_token.
    • A token stands for the user, not for a PHPSESSID, so xmoj's daily logout doesn't affect it.
    • It expires after 30 days without use (sliding; last_used is written at most once a day), or when Logout is called.
  • CheckToken
    • Accepts {Token}. The username always comes from the token row.
    • Legacy {SessionID, Username} still works unchanged for old script versions.
  • PHPSESSID only on demand
    • Scrapes that must read xmoj as the user call RequireSession(). With no cookie it returns {SessionRequired: true}, and the client retries once with {Token, SessionID}.
    • The cookie is verified and must belong to the token's user. It is held in memory only.
    • Today the only callers are UploadStd and GetStd.
  • Fewer scrapes
    • problem_score caches each user's best score. Scores only go up, so GetStd stops scraping (and stops needing a session) once a score clears the threshold.
    • IfUserExist reads userinfo.php without a cookie, since that page is public, and counts token holders as existing users.
  • WebSocket
    • ?Token= is a D1 lookup: no xmoj fetch, and no PHPSESSID in the URL.
    • ?SessionID=…&IssueToken=1 is for clients without a token yet. The worker mints one and the DO returns it in the connected message.
    • Plain ?SessionID= behaves exactly as before.
  • Cron deletes expired tokens.
  • Bug fix: the auth retry loop in Process() tested Data["Success"], which is always undefined. Every request therefore ran CheckToken three times. Now a legacy check runs once and retries only on failure, and a token check never retries.

Deploy order: run wrangler d1 migrations apply xmoj-bbs --remote before deploying. Merge and deploy this before the userscript PR, which needs Login.

Testing

  • npm test: 134 pass, 26 of them new. They cover:
    • token valid, unknown, malformed and expired
    • the last_used refresh
    • a cookie belonging to someone else being refused
    • SessionRequired
    • the score cache
    • IfUserExist sending no cookie
    • Login storing only the hash, and Logout
    • single-pass auth
    • WS with a token, with IssueToken, and legacy
    • a non-handshake request not minting a token
    • a client-supplied issuedToken being dropped
    • a lost or failed score-cache write not failing the request
  • wrangler deploy --dry-run bundles, and the migration applies to a local D1.
  • I ran the userscript's real auth/socket code against wrangler dev, with a fake PHPSESSID seeded into the local phpsessid cache, and checked:
    • one shared exchange
    • no PHPSESSID on later calls
    • a revoked or unknown token being replaced transparently
    • the GetStd session retry
    • the WS connecting with a token
    • a waiter falling back to Login when the WS can't mint
    • old-format requests still working
  • Live run: wrangler dev with a real xmoj account (zhuchenrui2) and the userscript's own auth code. Checked:
    • the WS minting a token through live profile.php
    • GetStd asking for the cookie and scraping live status.php
    • the second GetStd served from problem_score with no cookie
    • the WS reconnecting with the token
    • Logout

Does this PR need a documentation update?:

No. CLAUDE.md is updated in this PR.


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributor's guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against. (dev for XMOJ-Script, master for everything else).
  2. I have commented on my proposed changes within the code.
  3. I have tested my changes.
  4. I am willing to help maintain this change if there are issues with it later.
  5. It is compatible with the GNU General Public License v3.0
  6. I have squashed any insignificant commits. (git rebase)
  7. I have checked that another pull request for this purpose does not exist.
  8. I have considered and confirmed that this submission will be valuable to others.
  9. I accept that this submission may not be used, and the pull request can be closed at the will of the maintainer.
  10. I give this submission freely and claim no ownership to its content.
  11. I have verified that my changes work correctly in both the new UI and the old/classic UI. (Backend only, so there is no UI here. Item 11 applies to the userscript PR, Authenticate to the backend with our own session token XMOJ-Script#1064.)

  • I have read the above and my PR is ready for review. Check this box to confirm

🤖 Generated with Claude Code

https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

Summary by Sourcery

Replace per-request PHPSESSID verification with backend-issued session tokens while preserving legacy authentication and reducing authenticated XMOJ requests.

New Features:

  • Issue backend session tokens in exchange for verified PHPSESSIDs, with sliding expiration and explicit logout revocation.
  • Support token-based authentication for API requests and notification WebSockets while retaining legacy PHPSESSID authentication.
  • Request PHPSESSIDs only for operations that require authenticated XMOJ scraping, including transparent session retries.

Bug Fixes:

  • Prevent redundant authentication retries for token requests and avoid repeated legacy authentication checks.
  • Ensure supplied PHPSESSIDs are validated against the user associated with the backend token.

Enhancements:

  • Cache per-user problem scores to reduce authenticated XMOJ scraping and allow standard-solution access without a session once thresholds are met.
  • Reduce PHPSESSID exposure by omitting cookies from public XMOJ requests and storing only hashed backend tokens.
  • Clean up expired session tokens during scheduled maintenance.

Deployment:

  • Add the D1 migration for session tokens and cached problem scores.

Documentation:

  • Update authentication and database documentation for backend-issued tokens, on-demand sessions, and score caching.

Tests:

  • Add coverage for token lifecycle, session binding, WebSocket authentication, session-required retries, score caching, and single-pass authentication.

Clients swap a verified PHPSESSID for a token once (Login), then send only
the token. Checking it is a D1 lookup, so requests no longer wait on slow
fetches to xmoj, and we stop receiving the user's cookie on every call.
Tokens stand for the user, so they survive xmoj's daily logouts; they
expire after 30 days unused or on Logout.

Scrapes that must read xmoj as the user call RequireSession, which asks the
client to retry once with its PHPSESSID. GetStd caches scores so it rarely
needs one, and IfUserExist reads the public userinfo.php without a cookie.
The notification socket accepts a token, and can mint one for a client that
connects with its PHPSESSID and IssueToken=1. Legacy {SessionID, Username}
auth is unchanged for old clients.

Also fixes the auth retry loop, which tested Data.Success (always
undefined) and so ran CheckToken three times on every request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A
@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sorry @boomzero, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 15 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T08:50:36.844102Z 341c4b3 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

The backend now exchanges a verified PHPSESSID for a random hashed session token and uses D1 lookups for subsequent API and WebSocket authentication, retaining legacy compatibility while requesting PHPSESSIDs only for necessary xmoj scrapes. It also adds score caching, token cleanup, corrected auth retry behavior, migration support, documentation, and extensive tests.

Sequence diagram for session token exchange and API authentication

sequenceDiagram
    actor Client
    participant API as Backend API
    participant XMOJ
    participant D1

    Client->>API: CheckToken(SessionID, Username)
    API->>XMOJ: CheckSessionID
    XMOJ-->>API: Verified user
    Client->>API: Login
    API->>D1: IssueSessionToken
    D1-->>API: Store SHA3(token) and user_id
    API-->>Client: Token and ExpireTime
    Client->>API: CheckToken(Token)
    API->>D1: ResolveSessionToken
    D1-->>API: Username
    API-->>Client: Authenticated request
Loading

Sequence diagram for on-demand PHPSESSID during an xmoj scrape

sequenceDiagram
    actor Client
    participant API as Backend API
    participant D1
    participant XMOJ

    Client->>API: Request with Token
    API->>D1: ResolveSessionToken
    D1-->>API: Username
    API-->>Client: SessionRequired
    Client->>API: Retry with Token and SessionID
    API->>D1: ResolveSessionToken
    D1-->>API: Username
    API->>XMOJ: CheckSessionID for token user
    XMOJ-->>API: Verified PHPSESSID
    API->>XMOJ: UploadStd or GetStd scrape
    XMOJ-->>API: Scrape result
    API-->>Client: Result
Loading

Entity relationship diagram for session and score caching

erDiagram
    SESSION_TOKEN {
        string token_hash PK
        string user_id
        integer create_time
        integer last_used
    }
    PROBLEM_SCORE {
        string user_id PK
        integer problem_id PK
        integer score
    }
    SESSION_TOKEN }o--|| USER : authenticates
    PROBLEM_SCORE }o--|| USER : stores_best_score_for
Loading

File-Level Changes

Change Details Files
Replace per-request PHPSESSID validation with backend-issued, hashed, sliding-expiration session tokens.
  • Generate cryptographically random tokens on Login and persist only SHA3 hashes.
  • Resolve token ownership from D1, refresh usage daily, expire after 30 days, and revoke on Logout.
  • Keep legacy SessionID/Username authentication compatible while preventing token requests from overriding the token-derived username.
  • Validate an optional PHPSESSID only when a scrape explicitly requires it, and enforce that it belongs to the token owner.
Source/SessionToken.ts
Source/Process.ts
migrations/0006_add_session_token.sql
Reduce authenticated scraping and isolate PHPSESSID use to operations that need xmoj user context.
  • Add problem_score caching so passing scores avoid subsequent status.php scrapes.
  • Fetch public userinfo.php without forwarding a cookie.
  • Require and retry with a PHPSESSID only for UploadStd/GetStd flows that need logged-in scraping.
Source/Process.ts
migrations/0006_add_session_token.sql
CLAUDE.md
Extend notification WebSocket authentication to support backend tokens while preserving legacy clients.
  • Resolve ?Token= through D1 without contacting xmoj or exposing the token downstream.
  • Allow SessionID clients to request a token and return it once in the connected message.
  • Continue supporting plain SessionID connections and strip authentication parameters before forwarding to the Durable Object.
Source/index.ts
Source/NotificationManager.ts
Correct request authentication retry behavior and add scheduled token cleanup.
  • Perform token authentication once, while retaining a single retry for legacy authentication failures.
  • Delete tokens past their sliding expiration during the daily cron task.
Source/Process.ts
Source/index.ts
Add coverage for token lifecycle, session-on-demand behavior, WebSocket compatibility, caching, and single-pass authentication.
  • Test valid, malformed, unknown, expired, refreshed, revoked, and owner-bound tokens.
  • Test WebSocket token issuance/resolution, legacy behavior, scrape session requirements, score caching, cookie-free public fetches, Login storage, and authentication retry counts.
test/index.test.js
test/process.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread Source/index.ts Outdated
Comment thread Source/Process.ts Outdated
Comment thread migrations/0006_add_session_token.sql
Comment thread Source/index.ts
Comment thread Source/SessionToken.ts Outdated
Comment thread CLAUDE.md Outdated
Comment thread test/index.test.js
…fe score cache

- Only mint a token over the WebSocket for a real Upgrade request.
- Never forward a client-supplied issuedToken to the Durable Object.
- Caching a score never fails the request; losing the insert race raises
  the cached score instead of erroring on the primary key.
- Index session_token.last_used for the daily cleanup.
- Drop ExpireTime from Login: the lifetime slides, so it went stale.
- Tests match token lookups on the bound hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread Source/Process.ts
If our conditional update changes nothing, a concurrent request already
cached a score at least as high, from a newer scrape. Return that rather
than our stale one, so an eligible user isn't refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: Introduces backend-issued session tokens replacing per-request PHPSESSID checks, with new D1 tables, WebSocket token auth, and an auth retry-loop fix.

Turn on auto-fix | Re-trigger cubic

@boomzero
boomzero merged commit 71c553c into master Oct 7, 2026
7 checks passed
@boomzero
boomzero deleted the feat/session-token branch October 7, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant