authentication.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
  61. except (TypeError, UnicodeDecodeError, binascii.Error):
  62. msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
  63. raise exceptions.AuthenticationFailed(msg)
  64. userid, password = auth_parts[0], auth_parts[2]
  65. return self.authenticate_credentials(userid, password, request)
  66. def authenticate_credentials(self, userid, password, request=None):
  67. """
  68. Authenticate the userid and password against username and password
  69. with optional request for context.
  70. """
  71. credentials = {
  72. get_user_model().USERNAME_FIELD: userid,
  73. 'password': password
  74. }
  75. user = authenticate(request=request, **credentials)
  76. if user is None:
  77. raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
  78. if not user.is_active:
  79. raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
  80. return (user, None)
  81. def authenticate_header(self, request):
  82. return 'Basic realm="%s"' % self.www_authenticate_realm
  83. class SessionAuthentication(BaseAuthentication):
  84. """
  85. Use Django's session framework for authentication.
  86. """
  87. def authenticate(self, request):
  88. """
  89. Returns a `User` if the request session currently has a logged in user.
  90. Otherwise returns `None`.
  91. """
  92. # Get the session-based user from the underlying HttpRequest object
  93. user = getattr(request._request, 'user', None)
  94. # Unauthenticated, CSRF validation not required
  95. if not user or not user.is_active:
  96. return None
  97. self.enforce_csrf(request)
  98. # CSRF passed with authenticated user
  99. return (user, None)
  100. def enforce_csrf(self, request):
  101. """
  102. Enforce CSRF validation for session based authentication.
  103. """
  104. check = CSRFCheck()
  105. # populates request.META['CSRF_COOKIE'], which is used in process_view()
  106. check.process_request(request)
  107. reason = check.process_view(request, None, (), {})
  108. if reason:
  109. # CSRF failed, bail with explicit error message
  110. raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
  111. class TokenAuthentication(BaseAuthentication):
  112. """
  113. Simple token based authentication.
  114. Clients should authenticate by passing the token key in the "Authorization"
  115. HTTP header, prepended with the string "Token ". For example:
  116. Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
  117. """
  118. keyword = 'Token'
  119. model = None
  120. def get_model(self):
  121. if self.model is not None:
  122. return self.model
  123. from rest_framework.authtoken.models import Token
  124. return Token
  125. """
  126. A custom token model may be used, but must have the following properties.
  127. * key -- The string identifying the token
  128. * user -- The user to which the token belongs
  129. """
  130. def authenticate(self, request):
  131. auth = get_authorization_header(request).split()
  132. if not auth or auth[0].lower() != self.keyword.lower().encode():
  133. return None
  134. if len(auth) == 1:
  135. msg = _('Invalid token header. No credentials provided.')
  136. raise exceptions.AuthenticationFailed(msg)
  137. elif len(auth) > 2:
  138. msg = _('Invalid token header. Token string should not contain spaces.')
  139. raise exceptions.AuthenticationFailed(msg)
  140. try:
  141. token = auth[1].decode()
  142. except UnicodeError:
  143. msg = _('Invalid token header. Token string should not contain invalid characters.')
  144. raise exceptions.AuthenticationFailed(msg)
  145. return self.authenticate_credentials(token)
  146. def authenticate_credentials(self, key):
  147. model = self.get_model()
  148. try:
  149. token = model.objects.select_related('user').get(key=key)
  150. except model.DoesNotExist:
  151. raise exceptions.AuthenticationFailed(_('Invalid token.'))
  152. if not token.user.is_active:
  153. raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
  154. return (token.user, token)
  155. def authenticate_header(self, request):
  156. return self.keyword
  157. class RemoteUserAuthentication(BaseAuthentication):
  158. """
  159. REMOTE_USER authentication.
  160. To use this, set up your web server to perform authentication, which will
  161. set the REMOTE_USER environment variable. You will need to have
  162. 'django.contrib.auth.backends.RemoteUserBackend in your
  163. AUTHENTICATION_BACKENDS setting
  164. """
  165. # Name of request header to grab username from. This will be the key as
  166. # used in the request.META dictionary, i.e. the normalization of headers to
  167. # all uppercase and the addition of "HTTP_" prefix apply.
  168. header = "REMOTE_USER"
  169. def authenticate(self, request):
  170. user = authenticate(remote_user=request.META.get(self.header))
  171. if user and user.is_active:
  172. return (user, None)