1
1
Authentication
2
2
==============
3
3
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.
6
6
7
7
In most practical use cases, WebSocket servers need to authenticate clients in
8
8
order to route communications appropriately and securely.
9
9
10
- :rfc: `6455 ` stays elusive when it comes to authentication:
10
+ :rfc: `6455 ` remains elusive when it comes to authentication:
11
11
12
12
This protocol doesn't prescribe any particular way that servers can
13
13
authenticate clients during the WebSocket handshake. The WebSocket
@@ -26,8 +26,8 @@ System design
26
26
27
27
Consider a setup where the WebSocket server is separate from the HTTP server.
28
28
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.
31
31
32
32
The following diagram illustrates the authentication flow.
33
33
@@ -82,8 +82,8 @@ WebSocket server.
82
82
credentials would be a session identifier or a serialized, signed session.
83
83
84
84
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.
87
87
88
88
The proper workaround consists in:
89
89
@@ -108,13 +108,11 @@ WebSocket server.
108
108
109
109
Letting the browser perform HTTP Basic Auth is a nice idea in theory.
110
110
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:
112
112
113
- As of May 2021:
113
+ * Chrome behaves as expected.
114
114
115
- * Chrome 90 behaves as expected.
116
-
117
- * Firefox 88 caches credentials too aggressively.
115
+ * Firefox caches credentials too aggressively.
118
116
119
117
When connecting again to the same server with new credentials, it reuses
120
118
the old credentials, which may be expired, resulting in an HTTP 401. Then
@@ -123,7 +121,7 @@ WebSocket server.
123
121
When tokens are short-lived or single-use, this bug produces an
124
122
interesting effect: every other WebSocket connection fails.
125
123
126
- * Safari 14 ignores credentials entirely .
124
+ * Safari behaves as expected .
127
125
128
126
Two other options are off the table:
129
127
@@ -142,8 +140,10 @@ Two other options are off the table:
142
140
143
141
While this is suggested by the RFC, installing a TLS certificate is too far
144
142
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 :-)
147
147
148
148
Let's experiment!
149
149
-----------------
@@ -185,6 +185,8 @@ connection:
185
185
186
186
.. code-block :: python
187
187
188
+ from websockets.frames import CloseCode
189
+
188
190
async def first_message_handler (websocket ):
189
191
token = await websocket.recv()
190
192
user = get_user(token)
@@ -212,24 +214,16 @@ the user. If authentication fails, it returns an HTTP 401:
212
214
213
215
.. code-block :: python
214
216
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 " )
226
221
227
- self .user = user
222
+ user = get_user(token)
223
+ if user is None :
224
+ return connection.respond(http.HTTPStatus.UNAUTHORIZED , " Invalid token\n " )
228
225
229
- async def query_param_handler (websocket ):
230
- user = websocket.user
231
-
232
- ...
226
+ connection.username = user
233
227
234
228
Cookie
235
229
......
@@ -260,27 +254,19 @@ the user. If authentication fails, it returns an HTTP 401:
260
254
261
255
.. code-block :: python
262
256
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
+ ...
277
260
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 " )
279
264
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 " )
282
268
283
- ...
269
+ connection.username = user
284
270
285
271
User information
286
272
................
@@ -303,24 +289,12 @@ the user. If authentication fails, it returns an HTTP 401:
303
289
304
290
.. code-block :: python
305
291
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
316
293
317
- self .user = user
318
- return True
294
+ def check_credentials ( username , password ):
295
+ return username == get_user(password)
319
296
320
- async def user_info_handler (websocket ):
321
- user = websocket.user
322
-
323
- ...
297
+ basic_auth = websockets_basic_auth(check_credentials = check_credentials)
324
298
325
299
Machine-to-machine authentication
326
300
---------------------------------
@@ -334,11 +308,9 @@ To authenticate a websockets client with HTTP Basic Authentication
334
308
335
309
.. code-block :: python
336
310
337
- from websockets.legacy .client import connect
311
+ from websockets.asyncio .client import connect
338
312
339
- async with connect(
340
- f " wss:// { username} : { password} @example.com "
341
- ) as websocket:
313
+ async with connect(f " wss:// { username} : { password} @.../ " ) as websocket:
342
314
...
343
315
344
316
(You must :func: `~urllib.parse.quote ` ``username `` and ``password `` if they
@@ -349,10 +321,8 @@ To authenticate a websockets client with HTTP Bearer Authentication
349
321
350
322
.. code-block :: python
351
323
352
- from websockets.legacy .client import connect
324
+ from websockets.asyncio .client import connect
353
325
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:
358
328
...
0 commit comments