authentication.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """
  2. Provides various authentication policies.
  3. """
  4. import base64
  5. import binascii
  6. from django.contrib.auth import authenticate, get_user_model
  7. from django.middleware.csrf import CsrfViewMiddleware
  8. from django.utils.translation import gettext_lazy as _
  9. from rest_framework import HTTP_HEADER_ENCODING, exceptions
  10. def get_authorization_header(request):
  11. """
  12. Return request's 'Authorization:' header, as a bytestring.
  13. Hide some test client ickyness where the header can be unicode.
  14. """
  15. auth = request.META.get('HTTP_AUTHORIZATION', b'')
  16. if isinstance(auth, str):
  17. # Work around django test client oddness
  18. auth = auth.encode(HTTP_HEADER_ENCODING)
  19. return auth
  20. class CSRFCheck(CsrfViewMiddleware):
  21. def _reject(self, request, reason):
  22. # Return the failure reason instead of an HttpResponse
  23. return reason
  24. class BaseAuthentication:
  25. """
  26. All authentication classes should extend BaseAuthentication.
  27. """
  28. def authenticate(self, request):
  29. """
  30. Authenticate the request and return a two-tuple of (user, token).
  31. """
  32. raise NotImplementedError(".authenticate() must be overridden.")
  33. def authenticate_header(self, request):
  34. """
  35. Return a string to be used as the value of the `WWW-Authenticate`
  36. header in a `401 Unauthenticated` response, or `None` if the
  37. authentication scheme should return `403 Permission Denied` responses.
  38. """
  39. pass
  40. class BasicAuthentication(BaseAuthentication):
  41. """
  42. HTTP Basic authentication against username/password.
  43. """
  44. www_authenticate_realm = 'api'
  45. def authenticate(self, request):
  46. """
  47. Returns a `User` if a correct username and password have been supplied
  48. using HTTP Basic authentication. Otherwise returns `None`.
  49. """
  50. auth = get_authorization_header(request).split()
  51. if not auth or auth[0].lower() != b'basic':
  52. return None
  53. if len(auth) == 1:
  54. msg = _('Invalid basic header. No credentials provided.')
  55. raise exceptions.AuthenticationFailed(msg)
  56. elif len(auth) > 2:
  57. msg = _('Invalid basic header. Credentials string should not contain spaces.')
  58. raise exceptions.AuthenticationFailed(msg)
  59. try:
  60. try:
  61. auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
  62. except UnicodeDecodeError:
  63. auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
  64. auth_parts = auth_decoded.partition(':')
  65. except (TypeError, UnicodeDecodeError, binascii.Error):
  66. msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
  67. raise exceptions.AuthenticationFailed(msg)
  68. userid, password = auth_parts[0], auth_parts[2]
  69. return self.authenticate_credentials(userid, password, request)
  70. def authenticate_credentials(self, userid, password, request=None):
  71. """
  72. Authenticate the userid and password against username and password
  73. with optional request for context.
  74. """
  75. credentials = {
  76. get_user_model().USERNAME_FIELD: userid,
  77. 'password': password
  78. }
  79. user = authenticate(request=request, **credentials)
  80. if user is None:
  81. raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
  82. if not user.is_active:
  83. raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
  84. return (user, None)
  85. def authenticate_header(self, request):
  86. return 'Basic realm="%s"' % self.www_authenticate_realm
  87. class SessionAuthentication(BaseAuthentication):
  88. """
  89. Use Django's session framework for authentication.
  90. """
  91. def authenticate(self, request):
  92. """
  93. Returns a `User` if the request session currently has a logged in user.
  94. Otherwise returns `None`.
  95. """
  96. # Get the session-based user from the underlying HttpRequest object
  97. user = getattr(request._request, 'user', None)
  98. # Unauthenticated, CSRF validation not required
  99. if not user or not user.is_active:
  100. return None
  101. self.enforce_csrf(request)
  102. # CSRF passed with authenticated user
  103. return (user, None)
  104. def enforce_csrf(self, request):
  105. """
  106. Enforce CSRF validation for session based authentication.
  107. """
  108. def dummy_get_response(request): # pragma: no cover
  109. return None
  110. check = CSRFCheck(dummy_get_response)
  111. # populates request.META['CSRF_COOKIE'], which is used in process_view()
  112. check.process_request(request)
  113. reason = check.process_view(request, None, (), {})
  114. if reason:
  115. # CSRF failed, bail with explicit error message
  116. raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
  117. class TokenAuthentication(BaseAuthentication):
  118. """
  119. Simple token based authentication.
  120. Clients should authenticate by passing the token key in the "Authorization"
  121. HTTP header, prepended with the string "Token ". For example:
  122. Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
  123. """
  124. keyword = 'Token'
  125. model = None
  126. def get_model(self):
  127. if self.model is not None:
  128. return self.model
  129. from rest_framework.authtoken.models import Token
  130. return Token
  131. """
  132. A custom token model may be used, but must have the following properties.
  133. * key -- The string identifying the token
  134. * user -- The user to which the token belongs
  135. """
  136. def authenticate(self, request):
  137. auth = get_authorization_header(request).split()
  138. if not auth or auth[0].lower() != self.keyword.lower().encode():
  139. return None
  140. if len(auth) == 1:
  141. msg = _('Invalid token header. No credentials provided.')
  142. raise exceptions.AuthenticationFailed(msg)
  143. elif len(auth) > 2:
  144. msg = _('Invalid token header. Token string should not contain spaces.')
  145. raise exceptions.AuthenticationFailed(msg)
  146. try:
  147. token = auth[1].decode()
  148. except UnicodeError:
  149. msg = _('Invalid token header. Token string should not contain invalid characters.')
  150. raise exceptions.AuthenticationFailed(msg)
  151. return self.authenticate_credentials(token)
  152. def authenticate_credentials(self, key):
  153. model = self.get_model()
  154. try:
  155. token = model.objects.select_related('user').get(key=key)
  156. except model.DoesNotExist:
  157. raise exceptions.AuthenticationFailed(_('Invalid token.'))
  158. if not token.user.is_active:
  159. raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
  160. return (token.user, token)
  161. def authenticate_header(self, request):
  162. return self.keyword
  163. class RemoteUserAuthentication(BaseAuthentication):
  164. """
  165. REMOTE_USER authentication.
  166. To use this, set up your web server to perform authentication, which will
  167. set the REMOTE_USER environment variable. You will need to have
  168. 'django.contrib.auth.backends.RemoteUserBackend in your
  169. AUTHENTICATION_BACKENDS setting
  170. """
  171. # Name of request header to grab username from. This will be the key as
  172. # used in the request.META dictionary, i.e. the normalization of headers to
  173. # all uppercase and the addition of "HTTP_" prefix apply.
  174. header = "REMOTE_USER"
  175. def authenticate(self, request):
  176. user = authenticate(request=request, remote_user=request.META.get(self.header))
  177. if user and user.is_active:
  178. return (user, None)