Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-auth/plain/auth/sessions.py: 54%
69 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
1from plain.csrf.middleware import rotate_token
2from plain.exceptions import ImproperlyConfigured
3from plain.packages import packages as plain_packages
4from plain.runtime import settings
5from plain.utils.crypto import constant_time_compare, salted_hmac
7from .signals import user_logged_in, user_logged_out
9USER_ID_SESSION_KEY = "_auth_user_id"
10USER_HASH_SESSION_KEY = "_auth_user_hash"
13def _get_user_id_from_session(request):
14 # This value in the session is always serialized to a string, so we need
15 # to convert it back to Python whenever we access it.
16 return get_user_model()._meta.pk.to_python(request.session[USER_ID_SESSION_KEY])
19def get_session_auth_hash(user):
20 """
21 Return an HMAC of the password field.
22 """
23 return _get_session_auth_hash(user)
26def get_session_auth_fallback_hash(user):
27 for fallback_secret in settings.SECRET_KEY_FALLBACKS:
28 yield _get_session_auth_hash(user, secret=fallback_secret)
31def _get_session_auth_hash(user, secret=None):
32 key_salt = "plain.auth.get_session_auth_hash"
33 return salted_hmac(
34 key_salt,
35 getattr(user, settings.AUTH_USER_SESSION_HASH_FIELD),
36 secret=secret,
37 algorithm="sha256",
38 ).hexdigest()
41def login(request, user):
42 """
43 Persist a user id and a backend in the request. This way a user doesn't
44 have to reauthenticate on every request. Note that data set during
45 the anonymous session is retained when the user logs in.
46 """
47 if settings.AUTH_USER_SESSION_HASH_FIELD:
48 session_auth_hash = get_session_auth_hash(user)
49 else:
50 session_auth_hash = ""
52 if USER_ID_SESSION_KEY in request.session:
53 if _get_user_id_from_session(request) != user.pk:
54 # To avoid reusing another user's session, create a new, empty
55 # session if the existing session corresponds to a different
56 # authenticated user.
57 request.session.flush()
58 elif session_auth_hash and not constant_time_compare(
59 request.session.get(USER_HASH_SESSION_KEY, ""), session_auth_hash
60 ):
61 # If the session hash does not match the current hash, reset the
62 # session. Most likely this means the password was changed.
63 request.session.flush()
64 else:
65 request.session.cycle_key()
67 request.session[USER_ID_SESSION_KEY] = user._meta.pk.value_to_string(user)
68 request.session[USER_HASH_SESSION_KEY] = session_auth_hash
69 if hasattr(request, "user"):
70 request.user = user
71 rotate_token(request)
72 user_logged_in.send(sender=user.__class__, request=request, user=user)
75def logout(request):
76 """
77 Remove the authenticated user's ID from the request and flush their session
78 data.
79 """
80 # Dispatch the signal before the user is logged out so the receivers have a
81 # chance to find out *who* logged out.
82 user = getattr(request, "user", None)
83 user_logged_out.send(sender=user.__class__, request=request, user=user)
84 request.session.flush()
85 if hasattr(request, "user"):
86 request.user = None
89def get_user_model():
90 """
91 Return the User model that is active in this project.
92 """
93 try:
94 return plain_packages.get_model(settings.AUTH_USER_MODEL, require_ready=False)
95 except ValueError:
96 raise ImproperlyConfigured(
97 "AUTH_USER_MODEL must be of the form 'package_label.model_name'"
98 )
99 except LookupError:
100 raise ImproperlyConfigured(
101 f"AUTH_USER_MODEL refers to model '{settings.AUTH_USER_MODEL}' that has not been installed"
102 )
105def get_user(request):
106 """
107 Return the user model instance associated with the given request session.
108 If no user is retrieved, return None.
109 """
110 if USER_ID_SESSION_KEY not in request.session:
111 return None
113 user_id = _get_user_id_from_session(request)
115 UserModel = get_user_model()
116 try:
117 user = UserModel._default_manager.get(pk=user_id)
118 except UserModel.DoesNotExist:
119 return None
121 # If the user models defines a specific field to also hash and compare
122 # (like password), then we verify that the hash of that field is still
123 # the same as when the session was created.
124 #
125 # If it has changed (i.e. password changed), then the session
126 # is no longer valid and cleared out.
127 if settings.AUTH_USER_SESSION_HASH_FIELD:
128 session_hash = request.session.get(USER_HASH_SESSION_KEY)
129 if not session_hash:
130 session_hash_verified = False
131 else:
132 session_auth_hash = get_session_auth_hash(user)
133 session_hash_verified = constant_time_compare(
134 session_hash, session_auth_hash
135 )
136 if not session_hash_verified:
137 # If the current secret does not verify the session, try
138 # with the fallback secrets and stop when a matching one is
139 # found.
140 if session_hash and any(
141 constant_time_compare(session_hash, fallback_auth_hash)
142 for fallback_auth_hash in get_session_auth_fallback_hash(user)
143 ):
144 request.session.cycle_key()
145 request.session[USER_HASH_SESSION_KEY] = session_auth_hash
146 else:
147 request.session.flush()
148 user = None
150 return user