Skip to content

Commit 21987f9

Browse files
committed
Migrate authentication experiment to new asyncio.
1 parent a5c8943 commit 21987f9

File tree

5 files changed

+127
-194
lines changed

5 files changed

+127
-194
lines changed

docs/topics/authentication.rst

+44-74
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Authentication
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

77
In most practical use cases, WebSocket servers need to authenticate clients in
88
order 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

2727
Consider 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

3232
The 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

128126
Two 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

148148
Let'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

Comments
 (0)