wip(sync): do not send token on query string

It's not safe, it's logged in and can be easily intercepted.

A bit of documentation on websocket auth:

https://websockets.readthedocs.io/en/stable/topics/authentication.html
This commit is contained in:
Yohan Boniface 2024-12-26 16:41:57 +01:00
parent e865c31d69
commit 8cf2451142
4 changed files with 16 additions and 26 deletions

View file

@ -17,8 +17,6 @@ urlpatterns = (re_path(r"ws/sync/(?P<map_id>\w+)/$", consumers.SyncConsumer.as_a
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
"http": django_asgi_app, "http": django_asgi_app,
"websocket": consumers.TokenMiddleware( "websocket": AllowedHostsOriginValidator(URLRouter(urlpatterns)),
AllowedHostsOriginValidator(URLRouter(urlpatterns))
),
} }
) )

View file

@ -7,7 +7,7 @@ export class WebSocketTransport {
this.receiver = messagesReceiver this.receiver = messagesReceiver
this.closeRequested = false this.closeRequested = false
this.websocket = new WebSocket(`${webSocketURI}?${authToken}`) this.websocket = new WebSocket(`${webSocketURI}`)
this.websocket.onopen = () => { this.websocket.onopen = () => {
this.send('JoinRequest', { token: authToken }) this.send('JoinRequest', { token: authToken })

View file

@ -14,21 +14,6 @@ from .payloads import (
) )
class TokenMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
signed = TimestampSigner().unsign_object(
scope["query_string"].decode(), max_age=30
)
user, map_id, permissions = signed.values()
if "edit" not in permissions:
raise ValueError("Invalid Token")
scope["user"] = user
return await self.app(scope, receive, send)
class SyncConsumer(AsyncWebsocketConsumer): class SyncConsumer(AsyncWebsocketConsumer):
@property @property
def peers(self): def peers(self):
@ -41,7 +26,7 @@ class SyncConsumer(AsyncWebsocketConsumer):
await self.channel_layer.group_add(self.map_id, self.channel_name) await self.channel_layer.group_add(self.map_id, self.channel_name)
await self.accept() await self.accept()
await self.send_peers_list() self.is_authenticated = False
async def disconnect(self, close_code): async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.map_id, self.channel_name) await self.channel_layer.group_discard(self.map_id, self.channel_name)
@ -68,6 +53,18 @@ class SyncConsumer(AsyncWebsocketConsumer):
await self.send(event["message"]) await self.send(event["message"])
async def receive(self, text_data): async def receive(self, text_data):
if not self.is_authenticated:
message = JoinRequest.model_validate_json(text_data)
signed = TimestampSigner().unsign_object(message.token, max_age=30)
user, map_id, permissions = signed.values()
if "edit" not in permissions:
return await self.disconnect()
response = JoinResponse(uuid=self.channel_name, peers=self.peers)
await self.send(response.model_dump_json())
await self.send_peers_list()
self.is_authenticated = True
return
if text_data == "ping": if text_data == "ping":
return await self.send("pong") return await self.send("pong")
@ -81,9 +78,6 @@ class SyncConsumer(AsyncWebsocketConsumer):
else: else:
match incoming.root: match incoming.root:
# Broadcast all operation messages to connected peers # Broadcast all operation messages to connected peers
case JoinRequest():
response = JoinResponse(uuid=self.channel_name, peers=self.peers)
await self.send(response.model_dump_json())
case OperationMessage(): case OperationMessage():
await self.broadcast(text_data) await self.broadcast(text_data)

View file

@ -31,9 +31,7 @@ class PeerMessage(BaseModel):
class Request(RootModel): class Request(RootModel):
"""Any message coming from the websocket should be one of these, and will be rejected otherwise.""" """Any message coming from the websocket should be one of these, and will be rejected otherwise."""
root: Union[PeerMessage, OperationMessage, JoinRequest] = Field( root: Union[PeerMessage, OperationMessage] = Field(discriminator="kind")
discriminator="kind"
)
class JoinResponse(BaseModel): class JoinResponse(BaseModel):