1
1
import inspect
2
2
import re
3
3
4
- from asgiref .sync import sync_to_async
5
-
6
4
from django .apps import apps as django_apps
7
5
from django .conf import settings
8
6
from django .core .exceptions import ImproperlyConfigured , PermissionDenied
@@ -40,6 +38,39 @@ def get_backends():
40
38
return _get_backends (return_tuples = False )
41
39
42
40
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
+
43
74
@sensitive_variables ("credentials" )
44
75
def _clean_credentials (credentials ):
45
76
"""
@@ -62,19 +93,21 @@ def _get_user_session_key(request):
62
93
return get_user_model ()._meta .pk .to_python (request .session [SESSION_KEY ])
63
94
64
95
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
+
65
105
@sensitive_variables ("credentials" )
66
106
def authenticate (request = None , ** credentials ):
67
107
"""
68
108
If the given credentials are valid, return a User object.
69
109
"""
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 ):
78
111
try :
79
112
user = backend .authenticate (request , ** credentials )
80
113
except PermissionDenied :
@@ -96,7 +129,23 @@ def authenticate(request=None, **credentials):
96
129
@sensitive_variables ("credentials" )
97
130
async def aauthenticate (request = None , ** credentials ):
98
131
"""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
+ )
100
149
101
150
102
151
def login (request , user , backend = None ):
@@ -125,23 +174,7 @@ def login(request, user, backend=None):
125
174
else :
126
175
request .session .cycle_key ()
127
176
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 )
145
178
146
179
request .session [SESSION_KEY ] = user ._meta .pk .value_to_string (user )
147
180
request .session [BACKEND_SESSION_KEY ] = backend
@@ -154,7 +187,36 @@ def login(request, user, backend=None):
154
187
155
188
async def alogin (request , user , backend = None ):
156
189
"""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 )
158
220
159
221
160
222
def logout (request ):
@@ -177,7 +239,19 @@ def logout(request):
177
239
178
240
async def alogout (request ):
179
241
"""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 ()
181
255
182
256
183
257
def get_user_model ():
@@ -243,7 +317,43 @@ def get_user(request):
243
317
244
318
async def aget_user (request ):
245
319
"""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 ()
247
357
248
358
249
359
def get_permission_codename (action , opts ):
0 commit comments