Skip to content

feat(signalling): add an optional shared token on the player port - #979

Merged
mcottontensor merged 1 commit into
EpicGames:masterfrom
marekl11:feat/player-token-gate
Aug 13, 2026
Merged

feat(signalling): add an optional shared token on the player port#979
mcottontensor merged 1 commit into
EpicGames:masterfrom
marekl11:feat/player-token-gate

Conversation

@marekl11

Copy link
Copy Markdown
Contributor

Relevant components:

  • Signalling server
  • Common library
  • Frontend library
  • Frontend UI library
  • Matchmaker
  • Platform scripts
  • SFU

Problem statement:

Docs/Security-Guidelines.md tells a deployment to authenticate at the WebSocket upgrade with a verifyClient on playerWsOptions, and that is the right place — it runs before the config message, so a refused peer is never sent peerConnectionOptions. But the signalling server gives you no way to reach that seam, so even the simplest version of it means forking the reference server.

--turn_secret made the gap more obvious. A time limited credential decides how long a leak stays usable, not who is issued one, and anything that can open a player socket is issued one.

Solution

--player_token (or --player_token_file) requires a player to present a token, as a ?token= query parameter or an Authorization: Bearer header. One that cannot is refused during the upgrade with 401, so it never becomes a connection and never receives the config message. Streamer and SFU connections are untouched, and with no token configured playerWsOptions is not set at all.

It is deliberately the least ambitious useful thing: one shared token, the same for everyone, with no identity and no expiry. Enough for a kiosk or an internal demo, and the documentation says plainly that anything needing sessions or identity should supply its own verifyClient instead.

Two implementation notes, since both are easy to get wrong in a security feature. A request target that will not parse is refused rather than allowed to throw — Node's HTTP parser accepts targets new URL() rejects, and a throw inside verifyClient escapes through ws's upgrade handler and ends the process. And the token is removed from an accepted request before SignallingServer logs the URL it connected with, because that log is rotated and backed up.

Two things I would rather raise than have found:

This is authentication, and the guidelines said the project ships none. I have amended that sentence rather than leave it contradicting the new section below it. I think it belongs here for the same reason --turn_secret does — the library keeps the policy-free hook, the application implements the common case — but if you would rather the reference server stayed clear of it, I would genuinely rather hear that than have it merged reluctantly.

The bundled frontend cannot use it unaided. It builds its signalling URL from the page's protocol, host and port only, so a ?token= on the page address never reaches the socket. The docs give the ?ss= form that works today. Teaching Config.ts to carry the parameter through would make the obvious thing work, but it couples the frontend library to a flag name in the application, so I left it out — happy to add it if you would prefer.

Documentation

A subsection under the upgrade hook in Docs/Security-Guidelines.md: what the flag is, what it is not, and the caveats worth knowing — it gates the player port only, nothing rate limits guessing, and a query string is visible to anything that logs URLs. It also notes that --rest_api's GET /api/config already discloses peer options without a token. SignallingWebServer/README.md lists both options.

Test Plan and Compatibility

39 unit tests: bypass attempts, both transports, malformed targets, and the URL rewriting. Also run against a live server — no token, a wrong one, a prefix and an empty one are each refused; both transports connect; every malformed target is refused with the server still serving afterwards; and the connection log shows the token absent. With no token configured, all of the same requests connect exactly as they do on master.

npm run build, npm run lint and npm run test pass for SignallingWebServer.

Adds --player_token / --player_token_file, checked with the
playerWsOptions.verifyClient seam the security guidelines already
recommend. A player that cannot present it is refused during the HTTP
upgrade with 401, so it never becomes a connection and is never sent the
config message - which is where the peer options, and any TURN
credential in them, would otherwise be handed over.

The token is accepted as a ?token= query parameter or an Authorization:
Bearer header, either of them, and compared over SHA-256 digests so that
neither its length nor the position of the first difference is
observable.

Four things the mechanism has to get right, each covered by tests:

  - A request target that is not a parseable URL fails closed. Node's
    HTTP parser accepts targets the URL constructor rejects, and a throw
    inside verifyClient escapes through ws's upgrade handler and ends the
    process - so "//" would be an unauthenticated remote kill.
  - The reader and the remover agree on what the token parameter is, in
    both directions. The token is spliced out of an accepted request's
    target because the signalling server logs it and that log is rotated
    and backed up; if the two disagreed, an accepted request would keep
    its credential in the line that gets written. Hence reading every
    token parameter rather than the first, and matching the URL parser's
    treatment of encoded and control characters in a key. Splicing rather
    than rebuilding the target leaves every other parameter exactly as
    the client wrote it, since the request is handed to consumer code.
  - A misconfigured token fails closed and says so. A config file is
    written by hand, so a value that cannot be a token refuses to start
    rather than leaving the port open, and one that can be coerced into
    a token says which text it became.
  - Refusals are logged with their source address. The player port is
    not covered by the HTTP rate limiter, which only sees request
    events, so guessing is cheap and an operator needs to be able to see
    it happening. A token short enough to matter is warned about at
    startup.

Streamer and SFU connections are unaffected, and no player connection is
refused when no token is supplied. Two things do change for a deployment
that uses --turn_secret without a token: it now warns at startup that
TURN credentials are issued to anyone, and the interactive config dump
redacts turn_secret as the startup dump already did.
@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b4b134f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@epicgames-ps/wilbur Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@mcottontensor
mcottontensor merged commit 93fb57a into EpicGames:master Aug 13, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants