Skip to content

Authenticate to the backend with our own session token - #1064

Merged
boomzero merged 21 commits into
devfrom
session-token
Oct 7, 2026
Merged

boomzero merged 21 commits into
devfrom
session-token

Conversation

@boomzero

@boomzero boomzero commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

What does this PR aim to accomplish?:

This is the client side of XMOJ-Script-dev/XMOJ-bbs#75. It is safe to ship in either order. Against a backend without Login (before #75 deploys, or after a rollback), the script falls back to the old {SessionID, Username} auth for that page load.

Today every backend request sends the user's raw PHPSESSID, and the backend checks it against xmoj, which is slow. With this PR the script trades the PHPSESSID once for a token issued by our backend, then sends only the token. That makes requests faster and means the backend sees users' cookies as rarely as possible.

How does this PR accomplish the above?:

  • Token handling
    • GetBackendToken() returns the stored token, or exchanges the PHPSESSID for one through Login. Concurrent callers share a single exchange.
    • The token lives in GM storage, which the page can't read, and is keyed to CurrentUsername, so switching accounts gets a new one.
    • xmoj's daily logout and the auto-login don't touch the token.
  • RequestAPI sends {Token}. It handles two replies:
    • TokenInvalid: drop the token, re-exchange, and retry once.
    • SessionRequired: retry once with {Token, SessionID}. Only UploadStd, and GetStd before a score is cached, ever do this.
    • GetNotice and GetAddOnScript are sent without auth, so they still work when logged out.
    • The HTTP code is moved into PostAPI without behaviour changes. The httpOnly cookie reset now runs only when a PHPSESSID is actually needed, instead of on every request.
  • Notification socket
    • Connects with ?Token=.
    • With no token yet, it connects with the PHPSESSID plus IssueToken=1 and stores the token from the connected message. Requests made meanwhile wait for it and fall back to Login if it doesn't arrive.
    • A token that fails 3 handshakes in a row is dropped, since browsers can't see the 401.
  • Backend without Login: if Login answers "访问的页面不存在", the script uses the old auth for the rest of the page load.
  • Failed exchanges: remembered for 30 s, so a page full of requests doesn't send the PHPSESSID once per request while xmoj is down.
  • 注销所有设备 (new menu entry, behind a confirmation): the dialog leads with "if you suspect someone else is using your account, change your XMOJ password first", because the button ends plugin sessions, not XMOJ ones, and anyone with the password can log back in. It then lists what the button does and doesn't do. On confirm it calls LogoutAll (Add LogoutAll: sign a user out of the backend on every device XMOJ-bbs#76), then logs this device out like 注销. Sockets the backend closes with 4001 drop their token, so their next connection re-verifies the xmoj session for a new token.
  • Debug logs: with DebugMode on, request, response and socket logs hide tokens and PHPSESSIDs, because people paste their console output when asking for help. A new setting next to 调试模式, 调试日志中隐藏登录凭证, is on by default; developers can turn it off to see the raw values.
  • 注销 revokes the token on the backend (Logout, 2 s timeout) before going to logout.php. It never exchanges a PHPSESSID just to revoke.

Testing

  • I ran the real auth and socket code from this file in Node, against wrangler dev of XMOJ-bbs#75, with stubbed GM APIs and a fake PHPSESSID seeded into the local session cache. All 11 checks passed:
    • one Login, then token-only requests
    • concurrent requests sharing one exchange
    • unknown and revoked tokens replaced transparently
    • GetStd sending the PHPSESSID only on the requested retry
    • GetNotice working logged out
    • the socket connecting with the token
    • a request waiting on the socket, then falling back to Login when the socket can't mint a token
    • logout revoking without a new exchange
  • Live run: the same code against wrangler dev of XMOJ-bbs#75, logged in to real xmoj as zhuchenrui2. On a fresh install:
    • the socket minted the token through live xmoj
    • a request made meanwhile waited for it and used it, with no Login
    • later requests carried only the token
    • GetStd sent the cookie only on the requested retry, and the second call was served from the score cache without it
    • reconnecting used the token
    • 注销 revoked it
  • 注销所有设备: tested in real Chrome against [Bug] 更新死循环 #76 on local wrangler dev. Another device's socket closed with 4001, its token was dropped, a copied token was refused, and the device, still logged in to xmoj, quietly got a new token and reconnected.
  • Old backend: ran against the backend's current master locally. Three concurrent requests made one Login probe, then used the old auth, and no later request probed again.
  • Cooldown: with a session xmoj rejects, two requests made a single Login.
  • Test suite: account-settings and profile-page run the source between let RequestAPI and let SyncSettingsToCloud. RequestAPI therefore opens the auth block, and their fixtures now give the user a stored token, as a logged-in user has.
  • Behaviour kept: with a stored token, requests go out synchronously as before. A non-object body such as null is still passed to the caller.
  • npm test: 99/99 locally.
  • UI harness, tests/userscript-harness.cjs, logged in, run in all four combinations:
    • MonochromeUI on and off
    • each on XMOJ's /web pages and its old pages (20 pages per run)
    • result: no page errors or dialogs, and all 6 old-to-/web redirects land correctly
    • compared with the same runs on dev, there are no new console errors apart from the blocked backend calls
    • Cannot read properties of undefined (reading 'EmailHash') also appears on dev. It comes from GetUserInfo returning undefined when xmoj's own userinfo.php fetch fails, so it predates this PR.

Does this PR need a documentation update?:

No


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. (Checked with the harness in all four combinations: MonochromeUI on/off × XMOJ /web and old pages. The harness blocks the backend, so a human should still click 注销 and open the BBS/mail pages once against the deployed backend.)

  • 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

Swap the PHPSESSID for a backend-issued token once, kept in GM storage,
and send only the token after that. The PHPSESSID is sent again only when
the backend asks for it (SessionRequired), on re-exchange, or when the
notification socket connects before we have a token, in which case the
socket fetches one for everyone. Concurrent requests share one exchange;
an invalid token is replaced transparently; logging out revokes it.

Needs XMOJ-Script-dev/XMOJ-bbs#75 deployed first.

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
Contributor

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 7 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-07T09:59:47.167321Z 705bc4d Manual request
ℹ️ 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.

@hendragon-bot hendragon-bot Bot added the user-script This issue or pull request is related to the main user script label Oct 7, 2026
@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Reworks client authentication around a backend-issued, GM-storage-backed session token, centralizes token-aware HTTP handling, adapts notification sockets to token authentication, and revokes the token during logout while retaining narrowly scoped PHPSESSID fallback behavior.

Sequence diagram for backend token request and retry flow

sequenceDiagram
    participant Client
    participant RequestAPI
    participant GMStorage
    participant Backend
    participant XMOJ

    Client->>RequestAPI: RequestAPI(Action, Data, CallBack, ErrorCallBack)
    RequestAPI->>GMStorage: GetStoredBackendToken()
    alt token missing or account changed
        RequestAPI->>XMOJ: GetPHPSESSID()
        RequestAPI->>Backend: PostAPI(Login, SessionID, Username)
        Backend-->>RequestAPI: Token
        RequestAPI->>GMStorage: StoreBackendToken(Token)
    end
    RequestAPI->>Backend: PostAPI(Action, Token, Data)
    alt TokenInvalid
        RequestAPI->>GMStorage: StoreBackendToken("")
        RequestAPI->>Backend: PostAPI(Login, SessionID, Username)
        Backend-->>RequestAPI: Replacement Token
        RequestAPI->>Backend: PostAPI(Action, Token, Data)
    else SessionRequired
        RequestAPI->>XMOJ: GetPHPSESSIDOrReset()
        RequestAPI->>Backend: PostAPI(Action, Token, SessionID, Data)
    end
    Backend-->>Client: Result
Loading

Sequence diagram for notification socket token authentication

sequenceDiagram
    participant Client
    participant Socket as NotificationSocket
    participant Backend
    participant GMStorage

    Client->>GMStorage: GetStoredBackendToken()
    alt token available
        Client->>Socket: ConnectNotificationSocket()
        Socket->>Backend: WebSocket /ws/notifications?Token=Token
    else token missing
        Client->>Socket: ConnectNotificationSocket()
        Socket->>Backend: WebSocket /ws/notifications?SessionID=PHPSESSID&IssueToken=1
        Backend-->>Socket: connected token
        Socket-->>Client: connected message
        Client->>GMStorage: StoreBackendToken(Token)
    end
    Backend-->>Socket: Notification messages
Loading

Sequence diagram for token revocation during logout

sequenceDiagram
    actor User
    participant Client
    participant GMStorage
    participant Backend
    participant XMOJ

    User->>Client: 注销
    Client->>GMStorage: GetStoredBackendToken()
    Client->>GMStorage: StoreBackendToken("")
    alt token exists
        Client->>Backend: PostAPI(Logout, Token)
        Backend-->>Client: Logout result or timeout
    end
    Client->>XMOJ: Navigate to logout.php
Loading

File-Level Changes

Change Details Files
Replace per-request PHPSESSID authentication with a cached, username-scoped backend session token.
  • Exchange the XMOJ session for a backend token on demand and share concurrent exchanges.
  • Store and invalidate the token through GM storage, isolating it from page scripts and account switches.
  • Retry once after invalid-token responses by transparently obtaining a replacement token.
XMOJ.user.js
Centralize backend HTTP requests and restrict raw session forwarding to operations that require XMOJ authentication.
  • Extract common POST, timeout, parsing, status, and error handling into PostAPI.
  • Send token authentication by default, while preserving unauthenticated notice/add-on requests.
  • Retry session-required operations once with both the token and PHPSESSID, and avoid resetting the cookie unless it is needed.
XMOJ.user.js
Update notification WebSocket authentication and token acquisition flows.
  • Connect with the backend token, or use PHPSESSID once to request token issuance when no token exists.
  • Coordinate socket token issuance with pending API requests and fall back to Login if issuance fails or times out.
  • Persist issued tokens and clear tokens after repeated pre-open handshake failures.
XMOJ.user.js
Revoke backend sessions during logout without creating a new authentication exchange.
  • Clear the locally stored token and attempt a short, best-effort Logout request.
  • Navigate to XMOJ logout only after revocation completes or times out/fails.
XMOJ.user.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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Deploying xmoj-script-dev-channel with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8e6b60d
Status: ✅  Deploy successful!
Preview URL: https://2e96db67.xmoj-script-dev-channel.pages.dev
Branch Preview URL: https://session-token.xmoj-script-dev-channel.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 365f11990d

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread XMOJ.user.js Outdated
boomzero and others added 4 commits October 7, 2026 16:55
The account and profile tests run the source between `let RequestAPI` and
`let SyncSettingsToCloud`, so RequestAPI now opens the auth block and the
token helpers follow it. With a stored token it sends synchronously, and a
throwing transport is caught on the async paths too. The test fixtures give
the user a stored token, as a logged-in user has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A
A body such as `null` parses fine but has no fields, and reading
result.Success on it threw, turning "服务器响应异常" into a generic
"请求失败". Only look for TokenInvalid/SessionRequired in real results.

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 5 files (changes from recent commits).

Requires human review: Replaces per-request PHPSESSID auth with backend-issued session tokens (exchange, retry/revocation, socket handshake). This changes authentication and data-handling policy and depends on deploying the backend first, so it needs human sign-off.

Turn on auto-fix | Re-trigger cubic

@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.

Review completed against the latest diff

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

Turn on auto-fix | Re-trigger cubic

Comment thread XMOJ.user.js
Comment thread XMOJ.user.js
Comment thread XMOJ.user.js Outdated
boomzero and others added 3 commits October 7, 2026 17:05
An empty document.cookie PHPSESSID means the cookie is httpOnly, which
GetPHPSESSIDOrReset is already fixing with a reload; the user is logged in.

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: Auto-approval blocked by 3 unresolved issues from previous reviews.

Turn on auto-fix | Re-trigger cubic

boomzero and others added 2 commits October 7, 2026 17:13
…r Login

A script that updates before the backend deploys (or after a backend
rollback) got "访问的页面不存在" from Login and failed every request. If
Login doesn't exist, use {SessionID, Username} for the rest of the page
load, as that backend expects.

A failed exchange is now remembered for 30 seconds, so a page full of
requests doesn't send the PHPSESSID once per request while xmoj is down.

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 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Turn on auto-fix | Re-trigger cubic

Comment thread XMOJ.user.js Outdated
@boomzero

boomzero commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 705bc4d355

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread XMOJ.user.js Outdated
Comment thread XMOJ.user.js Outdated
boomzero and others added 2 commits October 7, 2026 19:15
A second logout entry, behind a confirmation, calls LogoutAll and then
logs this device out as 注销 does. Other devices' sockets are closed with
4001; they drop their token and, if still logged in to xmoj, quietly get
a new one. A token on its own is locked out.

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: Auto-approval blocked by 4 unresolved issues from previous reviews.

Turn on auto-fix | Re-trigger cubic

- Logout waits briefly for a token exchange under way and revokes what it
  produced; after that no exchange starts, and a token still arriving
  (slow Login, or the socket) is revoked instead of stored.
- A backend rolled back to before tokens answers "参数Token未知"; fall back
  to the old auth for the page load and keep the token for later.
- Failed socket handshakes no longer delete a token: after three, ask the
  backend over HTTP, and only a TokenInvalid answer drops it.
- The site's own logout (the classic logout.php link, the /web app's
  POST /api/logout) now revokes our token too, for users with ResetType off.
- DebugMode logs redact tokens and PHPSESSIDs.
- Only accept a Login reply that actually carries a token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A
@pull-request-size pull-request-size Bot added size/XL and removed size/L labels Oct 7, 2026

@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: Replaces per-request PHPSESSID authentication with backend-issued session tokens, including exchange, retry, socket handshake, and revocation. This is a deliberate authentication and data-handling policy change that needs human sign-off despite the old-backend fallback.

Turn on auto-fix | Re-trigger cubic

boomzero and others added 2 commits October 7, 2026 19:55
The button ends plugin sessions, not XMOJ ones, so someone who has the
password can simply log back in. The confirmation now leads with changing
the XMOJ password, then lists what the button does and does not do.

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: Replaces per-request PHPSESSID authentication with backend-issued session tokens, including exchange, retry, socket handshake, and revocation. This remains a deliberate authentication and data-handling policy change needing human sign-off despite the added warning dialog and tests.

View guided diff | Turn on auto-fix | Re-trigger cubic

boomzero and others added 2 commits October 7, 2026 20:17
New setting next to 调试模式, 调试日志中隐藏登录凭证, on by default so pasted
logs stay safe. Turning it off logs tokens and PHPSESSIDs as they are.
It is a sibling of DebugMode rather than a child: an entry with children
renders as a heading without its own checkbox.

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: Changes backend authentication to backend-issued session tokens and adds a “log out all devices” action. Human sign-off is needed for the authentication and data-handling policy change.

View guided diff | Turn on auto-fix | Re-trigger cubic

@boomzero
boomzero merged commit 146e184 into dev Oct 7, 2026
6 checks passed
@boomzero
boomzero deleted the session-token branch October 7, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL user-script This issue or pull request is related to the main user script

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant