You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auth.py 5.9KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. from django.conf import settings
  2. from django.contrib.auth import (
  3. BACKEND_SESSION_KEY,
  4. HASH_SESSION_KEY,
  5. SESSION_KEY,
  6. _get_backends,
  7. get_user_model,
  8. load_backend,
  9. user_logged_in,
  10. user_logged_out,
  11. )
  12. from django.contrib.auth.models import AnonymousUser
  13. from django.utils.crypto import constant_time_compare
  14. from django.utils.functional import LazyObject
  15. from django.utils.translation import LANGUAGE_SESSION_KEY
  16. from channels.db import database_sync_to_async
  17. from channels.middleware import BaseMiddleware
  18. from channels.sessions import CookieMiddleware, SessionMiddleware
  19. @database_sync_to_async
  20. def get_user(scope):
  21. """
  22. Return the user model instance associated with the given scope.
  23. If no user is retrieved, return an instance of `AnonymousUser`.
  24. """
  25. if "session" not in scope:
  26. raise ValueError(
  27. "Cannot find session in scope. You should wrap your consumer in SessionMiddleware."
  28. )
  29. session = scope["session"]
  30. user = None
  31. try:
  32. user_id = _get_user_session_key(session)
  33. backend_path = session[BACKEND_SESSION_KEY]
  34. except KeyError:
  35. pass
  36. else:
  37. if backend_path in settings.AUTHENTICATION_BACKENDS:
  38. backend = load_backend(backend_path)
  39. user = backend.get_user(user_id)
  40. # Verify the session
  41. if hasattr(user, "get_session_auth_hash"):
  42. session_hash = session.get(HASH_SESSION_KEY)
  43. session_hash_verified = session_hash and constant_time_compare(
  44. session_hash, user.get_session_auth_hash()
  45. )
  46. if not session_hash_verified:
  47. session.flush()
  48. user = None
  49. return user or AnonymousUser()
  50. @database_sync_to_async
  51. def login(scope, user, backend=None):
  52. """
  53. Persist a user id and a backend in the request.
  54. This way a user doesn't have to re-authenticate on every request.
  55. Note that data set during the anonymous session is retained when the user logs in.
  56. """
  57. if "session" not in scope:
  58. raise ValueError(
  59. "Cannot find session in scope. You should wrap your consumer in SessionMiddleware."
  60. )
  61. session = scope["session"]
  62. session_auth_hash = ""
  63. if user is None:
  64. user = scope.get("user", None)
  65. if user is None:
  66. raise ValueError(
  67. "User must be passed as an argument or must be present in the scope."
  68. )
  69. if hasattr(user, "get_session_auth_hash"):
  70. session_auth_hash = user.get_session_auth_hash()
  71. if SESSION_KEY in session:
  72. if _get_user_session_key(session) != user.pk or (
  73. session_auth_hash
  74. and not constant_time_compare(
  75. session.get(HASH_SESSION_KEY, ""), session_auth_hash
  76. )
  77. ):
  78. # To avoid reusing another user's session, create a new, empty
  79. # session if the existing session corresponds to a different
  80. # authenticated user.
  81. session.flush()
  82. else:
  83. session.cycle_key()
  84. try:
  85. backend = backend or user.backend
  86. except AttributeError:
  87. backends = _get_backends(return_tuples=True)
  88. if len(backends) == 1:
  89. _, backend = backends[0]
  90. else:
  91. raise ValueError(
  92. "You have multiple authentication backends configured and therefore must provide the `backend` "
  93. "argument or set the `backend` attribute on the user."
  94. )
  95. session[SESSION_KEY] = user._meta.pk.value_to_string(user)
  96. session[BACKEND_SESSION_KEY] = backend
  97. session[HASH_SESSION_KEY] = session_auth_hash
  98. scope["user"] = user
  99. # note this does not reset the CSRF_COOKIE/Token
  100. user_logged_in.send(sender=user.__class__, request=None, user=user)
  101. @database_sync_to_async
  102. def logout(scope):
  103. """
  104. Remove the authenticated user's ID from the request and flush their session data.
  105. """
  106. if "session" not in scope:
  107. raise ValueError(
  108. "Login cannot find session in scope. You should wrap your consumer in SessionMiddleware."
  109. )
  110. session = scope["session"]
  111. # Dispatch the signal before the user is logged out so the receivers have a
  112. # chance to find out *who* logged out.
  113. user = scope.get("user", None)
  114. if hasattr(user, "is_authenticated") and not user.is_authenticated:
  115. user = None
  116. if user is not None:
  117. user_logged_out.send(sender=user.__class__, request=None, user=user)
  118. # remember language choice saved to session
  119. language = session.get(LANGUAGE_SESSION_KEY)
  120. session.flush()
  121. if language is not None:
  122. session[LANGUAGE_SESSION_KEY] = language
  123. if "user" in scope:
  124. scope["user"] = AnonymousUser()
  125. def _get_user_session_key(session):
  126. # This value in the session is always serialized to a string, so we need
  127. # to convert it back to Python whenever we access it.
  128. return get_user_model()._meta.pk.to_python(session[SESSION_KEY])
  129. class UserLazyObject(LazyObject):
  130. """
  131. Throw a more useful error message when scope['user'] is accessed before it's resolved
  132. """
  133. def _setup(self):
  134. raise ValueError("Accessing scope user before it is ready.")
  135. class AuthMiddleware(BaseMiddleware):
  136. """
  137. Middleware which populates scope["user"] from a Django session.
  138. Requires SessionMiddleware to function.
  139. """
  140. def populate_scope(self, scope):
  141. # Make sure we have a session
  142. if "session" not in scope:
  143. raise ValueError(
  144. "AuthMiddleware cannot find session in scope. SessionMiddleware must be above it."
  145. )
  146. # Add it to the scope if it's not there already
  147. if "user" not in scope:
  148. scope["user"] = UserLazyObject()
  149. async def resolve_scope(self, scope):
  150. scope["user"]._wrapped = await get_user(scope)
  151. # Handy shortcut for applying all three layers at once
  152. AuthMiddlewareStack = lambda inner: CookieMiddleware(
  153. SessionMiddleware(AuthMiddleware(inner))
  154. )