11Authentication
22==============
33
4- The WebSocket protocol was designed for creating web applications that need
5- bidirectional communication between clients running in browsers and servers.
4+ The WebSocket protocol is designed for creating web applications that require
5+ bidirectional communication between browsers and servers.
66
77In most practical use cases, WebSocket servers need to authenticate clients in
88order to route communications appropriately and securely.
99
10- :rfc: `6455 ` stays elusive when it comes to authentication:
10+ :rfc: `6455 ` remains elusive when it comes to authentication:
1111
1212 This protocol doesn't prescribe any particular way that servers can
1313 authenticate clients during the WebSocket handshake. The WebSocket
@@ -26,8 +26,8 @@ System design
2626
2727Consider a setup where the WebSocket server is separate from the HTTP server.
2828
29- Most servers built with websockets to complement a web application adopt this
30- design because websockets doesn't aim at supporting HTTP.
29+ Most servers built with websockets adopt this design because they're a component
30+ in a web application and websockets doesn't aim at supporting HTTP.
3131
3232The following diagram illustrates the authentication flow.
3333
@@ -82,8 +82,8 @@ WebSocket server.
8282 credentials would be a session identifier or a serialized, signed session.
8383
8484 Unfortunately, when the WebSocket server runs on a different domain from
85- the web application, this idea bumps into the `Same-Origin Policy `_. For
86- security reasons, setting a cookie on a different origin is impossible.
85+ the web application, this idea hits the wall of the `Same-Origin Policy `_.
86+ For security reasons, setting a cookie on a different origin is impossible.
8787
8888 The proper workaround consists in:
8989
@@ -108,13 +108,11 @@ WebSocket server.
108108
109109 Letting the browser perform HTTP Basic Auth is a nice idea in theory.
110110
111- In practice it doesn't work due to poor support in browsers.
111+ In practice it doesn't work due to browser support limitations:
112112
113- As of May 2021:
113+ * Chrome behaves as expected.
114114
115- * Chrome 90 behaves as expected.
116-
117- * Firefox 88 caches credentials too aggressively.
115+ * Firefox caches credentials too aggressively.
118116
119117 When connecting again to the same server with new credentials, it reuses
120118 the old credentials, which may be expired, resulting in an HTTP 401. Then
@@ -123,7 +121,7 @@ WebSocket server.
123121 When tokens are short-lived or single-use, this bug produces an
124122 interesting effect: every other WebSocket connection fails.
125123
126- * Safari 14 ignores credentials entirely .
124+ * Safari behaves as expected .
127125
128126Two other options are off the table:
129127
@@ -142,8 +140,10 @@ Two other options are off the table:
142140
143141 While this is suggested by the RFC, installing a TLS certificate is too far
144142 from the mainstream experience of browser users. This could make sense in
145- high security contexts. I hope developers working on such projects don't
146- take security advice from the documentation of random open source projects.
143+ high security contexts.
144+
145+ I hope that developers working on projects in this category don't take
146+ security advice from the documentation of random open source projects :-)
147147
148148Let's experiment!
149149-----------------
@@ -185,6 +185,8 @@ connection:
185185
186186.. code-block :: python
187187
188+ from websockets.frames import CloseCode
189+
188190 async def first_message_handler (websocket ):
189191 token = await websocket.recv()
190192 user = get_user(token)
@@ -212,24 +214,16 @@ the user. If authentication fails, it returns an HTTP 401:
212214
213215.. code-block :: python
214216
215- from websockets.legacy.server import WebSocketServerProtocol
216-
217- class QueryParamProtocol (WebSocketServerProtocol ):
218- async def process_request (self , path , headers ):
219- token = get_query_parameter(path, " token" )
220- if token is None :
221- return http.HTTPStatus.UNAUTHORIZED , [], b " Missing token\n "
222-
223- user = get_user(token)
224- if user is None :
225- return http.HTTPStatus.UNAUTHORIZED , [], b " Invalid token\n "
217+ async def query_param_auth (connection , request ):
218+ token = get_query_param(request.path, " token" )
219+ if token is None :
220+ return connection.respond(http.HTTPStatus.UNAUTHORIZED , " Missing token\n " )
226221
227- self .user = user
222+ user = get_user(token)
223+ if user is None :
224+ return connection.respond(http.HTTPStatus.UNAUTHORIZED , " Invalid token\n " )
228225
229- async def query_param_handler (websocket ):
230- user = websocket.user
231-
232- ...
226+ connection.username = user
233227
234228 Cookie
235229......
@@ -260,27 +254,19 @@ the user. If authentication fails, it returns an HTTP 401:
260254
261255.. code-block :: python
262256
263- from websockets.legacy.server import WebSocketServerProtocol
264-
265- class CookieProtocol (WebSocketServerProtocol ):
266- async def process_request (self , path , headers ):
267- # Serve iframe on non-WebSocket requests
268- ...
269-
270- token = get_cookie(headers.get(" Cookie" , " " ), " token" )
271- if token is None :
272- return http.HTTPStatus.UNAUTHORIZED , [], b " Missing token\n "
273-
274- user = get_user(token)
275- if user is None :
276- return http.HTTPStatus.UNAUTHORIZED , [], b " Invalid token\n "
257+ async def cookie_auth (connection , request ):
258+ # Serve iframe on non-WebSocket requests
259+ ...
277260
278- self .user = user
261+ token = get_cookie(request.headers.get(" Cookie" , " " ), " token" )
262+ if token is None :
263+ return connection.respond(http.HTTPStatus.UNAUTHORIZED , " Missing token\n " )
279264
280- async def cookie_handler (websocket ):
281- user = websocket.user
265+ user = get_user(token)
266+ if user is None :
267+ return connection.respond(http.HTTPStatus.UNAUTHORIZED , " Invalid token\n " )
282268
283- ...
269+ connection.username = user
284270
285271 User information
286272................
@@ -303,24 +289,12 @@ the user. If authentication fails, it returns an HTTP 401:
303289
304290.. code-block :: python
305291
306- from websockets.legacy.auth import BasicAuthWebSocketServerProtocol
307-
308- class UserInfoProtocol (BasicAuthWebSocketServerProtocol ):
309- async def check_credentials (self , username , password ):
310- if username != " token" :
311- return False
312-
313- user = get_user(password)
314- if user is None :
315- return False
292+ from websockets.asyncio.server import basic_auth as websockets_basic_auth
316293
317- self .user = user
318- return True
294+ def check_credentials ( username , password ):
295+ return username == get_user(password)
319296
320- async def user_info_handler (websocket ):
321- user = websocket.user
322-
323- ...
297+ basic_auth = websockets_basic_auth(check_credentials = check_credentials)
324298
325299 Machine-to-machine authentication
326300---------------------------------
@@ -334,11 +308,9 @@ To authenticate a websockets client with HTTP Basic Authentication
334308
335309.. code-block :: python
336310
337- from websockets.legacy .client import connect
311+ from websockets.asyncio .client import connect
338312
339- async with connect(
340- f " wss:// { username} : { password} @example.com "
341- ) as websocket:
313+ async with connect(f " wss:// { username} : { password} @.../ " ) as websocket:
342314 ...
343315
344316 (You must :func: `~urllib.parse.quote ` ``username `` and ``password `` if they
@@ -349,10 +321,8 @@ To authenticate a websockets client with HTTP Bearer Authentication
349321
350322.. code-block :: python
351323
352- from websockets.legacy .client import connect
324+ from websockets.asyncio .client import connect
353325
354- async with connect(
355- " wss://example.com" ,
356- extra_headers = {" Authorization" : f " Bearer { token} " }
357- ) as websocket:
326+ headers = {" Authorization" : f " Bearer { token} " }
327+ async with connect(" wss://.../" , additional_headers = headers) as websocket:
358328 ...
0 commit comments