Skip to content

Commit 50f89ae

Browse files
bigfootjonsarahboyce
authored andcommitted
Fixed #35303 -- Implemented async auth backends and utils.
1 parent 4cad317 commit 50f89ae

File tree

17 files changed

+1285
-61
lines changed

17 files changed

+1285
-61
lines changed

django/contrib/auth/__init__.py

Lines changed: 141 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import inspect
22
import re
33

4-
from asgiref.sync import sync_to_async
5-
64
from django.apps import apps as django_apps
75
from django.conf import settings
86
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
@@ -40,6 +38,39 @@ def get_backends():
4038
return _get_backends(return_tuples=False)
4139

4240

41+
def _get_compatible_backends(request, **credentials):
42+
for backend, backend_path in _get_backends(return_tuples=True):
43+
backend_signature = inspect.signature(backend.authenticate)
44+
try:
45+
backend_signature.bind(request, **credentials)
46+
except TypeError:
47+
# This backend doesn't accept these credentials as arguments. Try
48+
# the next one.
49+
continue
50+
yield backend, backend_path
51+
52+
53+
def _get_backend_from_user(user, backend=None):
54+
try:
55+
backend = backend or user.backend
56+
except AttributeError:
57+
backends = _get_backends(return_tuples=True)
58+
if len(backends) == 1:
59+
_, backend = backends[0]
60+
else:
61+
raise ValueError(
62+
"You have multiple authentication backends configured and "
63+
"therefore must provide the `backend` argument or set the "
64+
"`backend` attribute on the user."
65+
)
66+
else:
67+
if not isinstance(backend, str):
68+
raise TypeError(
69+
"backend must be a dotted import path string (got %r)." % backend
70+
)
71+
return backend
72+
73+
4374
@sensitive_variables("credentials")
4475
def _clean_credentials(credentials):
4576
"""
@@ -62,19 +93,21 @@ def _get_user_session_key(request):
6293
return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])
6394

6495

96+
async def _aget_user_session_key(request):
97+
# This value in the session is always serialized to a string, so we need
98+
# to convert it back to Python whenever we access it.
99+
session_key = await request.session.aget(SESSION_KEY)
100+
if session_key is None:
101+
raise KeyError()
102+
return get_user_model()._meta.pk.to_python(session_key)
103+
104+
65105
@sensitive_variables("credentials")
66106
def authenticate(request=None, **credentials):
67107
"""
68108
If the given credentials are valid, return a User object.
69109
"""
70-
for backend, backend_path in _get_backends(return_tuples=True):
71-
backend_signature = inspect.signature(backend.authenticate)
72-
try:
73-
backend_signature.bind(request, **credentials)
74-
except TypeError:
75-
# This backend doesn't accept these credentials as arguments. Try
76-
# the next one.
77-
continue
110+
for backend, backend_path in _get_compatible_backends(request, **credentials):
78111
try:
79112
user = backend.authenticate(request, **credentials)
80113
except PermissionDenied:
@@ -96,7 +129,23 @@ def authenticate(request=None, **credentials):
96129
@sensitive_variables("credentials")
97130
async def aauthenticate(request=None, **credentials):
98131
"""See authenticate()."""
99-
return await sync_to_async(authenticate)(request, **credentials)
132+
for backend, backend_path in _get_compatible_backends(request, **credentials):
133+
try:
134+
user = await backend.aauthenticate(request, **credentials)
135+
except PermissionDenied:
136+
# This backend says to stop in our tracks - this user should not be
137+
# allowed in at all.
138+
break
139+
if user is None:
140+
continue
141+
# Annotate the user object with the path of the backend.
142+
user.backend = backend_path
143+
return user
144+
145+
# The credentials supplied are invalid to all backends, fire signal.
146+
await user_login_failed.asend(
147+
sender=__name__, credentials=_clean_credentials(credentials), request=request
148+
)
100149

101150

102151
def login(request, user, backend=None):
@@ -125,23 +174,7 @@ def login(request, user, backend=None):
125174
else:
126175
request.session.cycle_key()
127176

128-
try:
129-
backend = backend or user.backend
130-
except AttributeError:
131-
backends = _get_backends(return_tuples=True)
132-
if len(backends) == 1:
133-
_, backend = backends[0]
134-
else:
135-
raise ValueError(
136-
"You have multiple authentication backends configured and "
137-
"therefore must provide the `backend` argument or set the "
138-
"`backend` attribute on the user."
139-
)
140-
else:
141-
if not isinstance(backend, str):
142-
raise TypeError(
143-
"backend must be a dotted import path string (got %r)." % backend
144-
)
177+
backend = _get_backend_from_user(user=user, backend=backend)
145178

146179
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
147180
request.session[BACKEND_SESSION_KEY] = backend
@@ -154,7 +187,36 @@ def login(request, user, backend=None):
154187

155188
async def alogin(request, user, backend=None):
156189
"""See login()."""
157-
return await sync_to_async(login)(request, user, backend)
190+
session_auth_hash = ""
191+
if user is None:
192+
user = await request.auser()
193+
if hasattr(user, "get_session_auth_hash"):
194+
session_auth_hash = user.get_session_auth_hash()
195+
196+
if await request.session.ahas_key(SESSION_KEY):
197+
if await _aget_user_session_key(request) != user.pk or (
198+
session_auth_hash
199+
and not constant_time_compare(
200+
await request.session.aget(HASH_SESSION_KEY, ""),
201+
session_auth_hash,
202+
)
203+
):
204+
# To avoid reusing another user's session, create a new, empty
205+
# session if the existing session corresponds to a different
206+
# authenticated user.
207+
await request.session.aflush()
208+
else:
209+
await request.session.acycle_key()
210+
211+
backend = _get_backend_from_user(user=user, backend=backend)
212+
213+
await request.session.aset(SESSION_KEY, user._meta.pk.value_to_string(user))
214+
await request.session.aset(BACKEND_SESSION_KEY, backend)
215+
await request.session.aset(HASH_SESSION_KEY, session_auth_hash)
216+
if hasattr(request, "user"):
217+
request.user = user
218+
rotate_token(request)
219+
await user_logged_in.asend(sender=user.__class__, request=request, user=user)
158220

159221

160222
def logout(request):
@@ -177,7 +239,19 @@ def logout(request):
177239

178240
async def alogout(request):
179241
"""See logout()."""
180-
return await sync_to_async(logout)(request)
242+
# Dispatch the signal before the user is logged out so the receivers have a
243+
# chance to find out *who* logged out.
244+
user = getattr(request, "auser", None)
245+
if user is not None:
246+
user = await user()
247+
if not getattr(user, "is_authenticated", True):
248+
user = None
249+
await user_logged_out.asend(sender=user.__class__, request=request, user=user)
250+
await request.session.aflush()
251+
if hasattr(request, "user"):
252+
from django.contrib.auth.models import AnonymousUser
253+
254+
request.user = AnonymousUser()
181255

182256

183257
def get_user_model():
@@ -243,7 +317,43 @@ def get_user(request):
243317

244318
async def aget_user(request):
245319
"""See get_user()."""
246-
return await sync_to_async(get_user)(request)
320+
from .models import AnonymousUser
321+
322+
user = None
323+
try:
324+
user_id = await _aget_user_session_key(request)
325+
backend_path = await request.session.aget(BACKEND_SESSION_KEY)
326+
except KeyError:
327+
pass
328+
else:
329+
if backend_path in settings.AUTHENTICATION_BACKENDS:
330+
backend = load_backend(backend_path)
331+
user = await backend.aget_user(user_id)
332+
# Verify the session
333+
if hasattr(user, "get_session_auth_hash"):
334+
session_hash = await request.session.aget(HASH_SESSION_KEY)
335+
if not session_hash:
336+
session_hash_verified = False
337+
else:
338+
session_auth_hash = user.get_session_auth_hash()
339+
session_hash_verified = session_hash and constant_time_compare(
340+
session_hash, user.get_session_auth_hash()
341+
)
342+
if not session_hash_verified:
343+
# If the current secret does not verify the session, try
344+
# with the fallback secrets and stop when a matching one is
345+
# found.
346+
if session_hash and any(
347+
constant_time_compare(session_hash, fallback_auth_hash)
348+
for fallback_auth_hash in user.get_session_auth_fallback_hash()
349+
):
350+
await request.session.acycle_key()
351+
await request.session.aset(HASH_SESSION_KEY, session_auth_hash)
352+
else:
353+
await request.session.aflush()
354+
user = None
355+
356+
return user or AnonymousUser()
247357

248358

249359
def get_permission_codename(action, opts):

0 commit comments

Comments
 (0)