Skip to content

fix(serverdb): re-acquire the bearer token when a reconnect gets 401 (#39) - #113

Merged
thesprockee merged 3 commits into
mainfrom
fix/39-reconnect-expired-token
Oct 6, 2026
Merged

thesprockee merged 3 commits into
mainfrom
fix/39-reconnect-expired-token

Conversation

@thesprockee

Copy link
Copy Markdown
Member

Summary

  • Confirms and closes websocket_client: Authorization header is set once at Connect; reconnect with an expired token is unverified #39: the Authorization header is set once at Connect and never refreshed. ixwebsocket auto-reconnects by default and reuses the stored header on every retry (confirmed in vcpkg's ixwebsocket 11.4.6 source). An expired token means every reconnect gets HTTP 401 from Nakama (jwt.WithExpirationRequired, confirmed in the local nakama checkout) and nothing recovers it, since token-auth's refresh thread is skipped entirely in server mode (token_auth.cpp:906-911).
  • Where it actually bites: with the default exit-on-error setting, Update() sees the disconnect and starts shutdown before a reconnect matters. The silent forever-401-retry case needs -noexitonerror, or a reconnect firing while Update() isn't being called (a documented level-transition gap at gameserver.cpp:1012).
  • Fix: on a reconnect failure with HTTP 401 while a bearer token was in use, call a new AcquireServerDbToken() (extracted from RequestRegistration's existing refresh-then-password-auth logic, unchanged) and set the new header before the next attempt. Only 401 triggers it — network failures and 5xx don't hit the auth endpoint.

Test plan

  • New test_websocket_client_auth (4 cases) against a scripted loopback server: no-refresher reuses stale token; 401 gets a new token on next attempt; 503 doesn't trigger a refresh; failed refresh keeps the old token and logs it
  • Mutation checks: disabling the 401 hook fails 2 tests; firing on any HTTP error fails the 503 test
  • just verify green (capped CMAKE_BUILD_PARALLEL_LEVEL=4)

Not done

  • No live test holding a real game-server session past token expiry against production Nakama — the 401 path is proven against a scripted server only.

Found, not fixed here

  • src/runtime/server/telemetry_streamer.cpp:41-51 has the identical set-once-header + auto-reconnect pattern (token from config's telemetry_token or a ServerDB-token fallback). Same class of bug, left out of scope — obvious follow-up.

🤖 Generated with Claude Code

…39)

WebSocketClient::Connect handed the JWT to ixwebsocket once, through
setExtraHeaders. ixwebsocket 11.4.6 reconnects by calling
WebSocket::connect() again, which copies the stored _extraHeaders
(IXWebSocket.cpp:210). So every automatic reconnect presented the token
that was valid at the first Connect. Nakama rejects an expired JWT at the
upgrade with 401 (parseToken, jwt.WithExpirationRequired), and in server
mode nothing else refreshes the token (TokenAuth::Init returns early on
is_server). A ServerDB reconnect after the ~1h TTL could therefore never
succeed.

Now an Error message carrying HTTP 401 calls a token refresher, and the
next attempt presents the new header. The refresher is
AcquireServerDbToken, factored out of RequestRegistration unchanged:
refresh-token exchange first, then password auth. Only 401 triggers it,
so a network failure or a 5xx does not call the auth endpoint. The header
is written on ixwebsocket's own thread, the only thread that emits Error
and the one whose connect() reads the headers without a lock. Each step
logs: the rejection, a failed re-acquisition, and the replacement with
refresh_count.

test_websocket_client_auth drives the real client against a scripted
loopback upgrade server and records each handshake's Authorization
header. It pins the stale-header behaviour without a refresher, the 401
path that mints a new token, the 503 path that does not, and a failed
mint. Two mutants were checked: no hook turns two tests red, and
refreshing on any error turns the 503 test red. The target is added to
test-auth-unit.

Co-Authored-By: nevr-runtime <agents@sprock.io>
…red-token

* origin/main:
  fix(gameserver): drop const_cast on received ServerDB payloads (#43)
  fix(winhttp): report the server's reason phrase from get_StatusText (#27)

Co-Authored-By: nevr-runtime <agents@sprock.io>

# Conflicts:
#	justfile
…red-token

* origin/main:
  fix(server): read lobby entrants live instead of a copy taken at Initialize (#38)
  fix(abi): Lobby::EntrantData is 0xD8 bytes, not 0xA0 (#38)

Co-Authored-By: nevr-runtime <agents@sprock.io>

# Conflicts:
#	justfile
@thesprockee
thesprockee merged commit 24a2432 into main Oct 6, 2026
1 check failed
@thesprockee
thesprockee deleted the fix/39-reconnect-expired-token branch October 6, 2026 01:11
thesprockee added a commit that referenced this pull request Oct 6, 2026
…erdb-uri

Resolve justfile test-target list conflict from #113 landing on main:
keep both test_serverdb_uri (this branch) and test_websocket_client_auth
(main) in both test-auth-unit lists.

Co-Authored-By: nevr-runtime <agents@sprock.io>
thesprockee added a commit that referenced this pull request Oct 6, 2026
…d-registry-violation

Resolve src/runtime/CMakeLists.txt list conflict from #103/#104/#111/#113
landing on main: keep both the test_main_thread_handoff comment and the
test_winhttp_stub target block.

Co-Authored-By: nevr-runtime <agents@sprock.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

websocket_client: Authorization header is set once at Connect; reconnect with an expired token is unverified

1 participant