Skip to content

feat(ws): require a logged-in session for /ws when auth is enabled - #3324

Merged
vpetersson merged 2 commits into
masterfrom
siri-62-authenticate-ws
Sep 15, 2026
Merged

vpetersson merged 2 commits into
masterfrom
siri-62-authenticate-ws

Conversation

@vpetersson-bot

Copy link
Copy Markdown
Contributor

Issues Fixed

Follow-up from the review of PR 3308, accepted there as non-blocking. /ws was
unauthenticated — AllowedHostsOriginValidator only — so anyone who could reach
the device's port could hold a socket open and watch it. That PR narrowed its own
payload to a '*' sentinel, but the timing of each frame still leaked: an
anonymous listener could tell when the screen rotated. notify_asset_update also
emits real asset ids over the same socket on every write.

Description

/ws now follows the same authentication switch as the rest of the app:

  • django_project/asgi.py wraps the websocket router in Channels'
    AuthMiddlewareStack, so the handshake resolves scope['user'] from the
    session cookie the browser already sends. AllowedHostsOriginValidator stays
    outermost — a cross-origin handshake is refused before we spend a session
    lookup on it.
  • AssetConsumer.connect() refuses the handshake before accept() and before
    the channel joins WS_GROUP, so there is no frame left to time. Channels
    answers the upgrade with a 403.
  • The gate reads settings['auth_backend'], mirroring @authorized: with
    authentication disabled the endpoint stays open, exactly as every other surface
    on the device does. Devices on the default config are unaffected.

Session cookie only, deliberately. Every page that opens /ws extends
base.html and is itself behind @authorized, so an operator browser is the
only legitimate client; browsers can't set an Authorization header on a
new WebSocket() handshake anyway, so honouring the deprecated Basic path here
would only widen it to a surface that never had it.

Tests cover the gate directly and end-to-end through the real ASGI application
with a genuine session cookie — the latter is what pins that the middleware is
actually wired, not just that the consumer checks something.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

Hardware QA ran on the x86 testbed and a Pi 5, via the overlay method, 7/7 checks
passing on each: with auth off an anonymous socket opens and receives the
fan-out; with auth on an anonymous handshake is refused (403) while a real
login-form session opens the socket and still receives the fan-out; turning auth
back off reopens it. On the unpatched image the same probe confirmed the leak —
with auth on, an anonymous handshake was accepted (101) and received the
'*' frame. Both boards were restored to their pinned images and left with auth
off and no leftover users or assets.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 11, 2026 15:57
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner September 11, 2026 15:57
@vpetersson-bot vpetersson-bot self-assigned this Sep 11, 2026

Copilot AI 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.

🟡 Changes recommended

Existing sockets are authorized only at connection time, so enabling authentication or revoking a session does not remove already-connected listeners from WS_GROUP.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds session authentication to the /ws WebSocket endpoint when authentication is enabled, while preserving open access when disabled.

Changes:

  • Wraps WebSocket routing with AuthMiddlewareStack.
  • Rejects unauthenticated connections before joining the broadcast group.
  • Adds end-to-end tests, typing stubs, and QA/developer documentation.
File summaries
File Description
src/anthias_server/app/consumers.py Adds the WebSocket authorization gate.
src/anthias_server/django_project/asgi.py Wires session authentication into ASGI routing.
tests/test_consumers.py Tests authorization and real ASGI handshakes.
website/content/docs/qa-checklist.md Documents WebSocket authentication QA.
website/content/docs/developer-documentation.md Documents /ws authentication behavior.
stubs/channels-stubs/generic/websocket.pyi Types the consumer scope.
stubs/channels-stubs/auth.pyi Adds middleware typing.
stubs/asgiref-stubs/testing.pyi Types ApplicationCommunicator.
stubs/asgiref-stubs/py.typed Marks partial stubs.
stubs/asgiref-stubs/__init__.pyi Adds the partial stub package marker.
pyproject.toml Documents the added stubs.
Review details
  • Files reviewed: 10/11 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/anthias_server/app/consumers.py
Copilot AI review requested due to automatic review settings September 12, 2026 05:55

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

vpetersson-bot and others added 2 commits September 15, 2026 15:18
- Wrap the websocket router in AuthMiddlewareStack so the handshake
  resolves scope['user'] from the session cookie
- Refuse the handshake in AssetConsumer.connect() before accept() and
  before the channel joins WS_GROUP, so an unauthenticated listener
  can no longer time the device's writes
- Gate on settings['auth_backend'], matching @Authorized: with auth
  off the endpoint stays open, as every other surface does
- Cover the gate with unit tests plus end-to-end tests that drive the
  real ASGI app with a genuine session cookie

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
connect() decided authorization once, so a socket opened while auth was
off kept streaming after the operator turned auth on, and a socket
opened for an operator outlived the credentials it was accepted under.

- Re-check authorization per frame in asset_update(); stay silent
  rather than closing, since a close is itself an event the listener
  could time against the write being withheld
- Add force_disconnect()/disconnect_all() and fan it out from both
  settings-save paths so sockets re-handshake against the new state
- Have apply_auth_settings() report whether credentials in use were
  invalidated, so the HTML and DRF surfaces can't drift on when to
  reap sockets, and an unrelated save doesn't bounce them
- Share the after-close race matcher between the send and close paths

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot requested a lite review from Copilot September 15, 2026 15:19
@sonarqubecloud

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit 86f15c3 into master Sep 15, 2026
11 checks passed

Copilot AI 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.

🟡 Changes recommended

The unresolved critical credential-revocation issue and two moderate disconnect-order issues must be fixed.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 16/17 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread src/anthias_server/app/consumers.py
Comment thread src/anthias_server/api/views/v2.py
Comment thread src/anthias_server/app/views.py
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.

3 participants