authentication.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import base64
  2. import datetime
  3. from ipaddress import ip_address
  4. from django.contrib.auth.hashers import PBKDF2PasswordHasher
  5. from django.utils import timezone
  6. from rest_framework import exceptions, HTTP_HEADER_ENCODING
  7. from rest_framework.authentication import (
  8. BaseAuthentication,
  9. get_authorization_header,
  10. TokenAuthentication as RestFrameworkTokenAuthentication,
  11. BasicAuthentication,
  12. )
  13. from desecapi.models import Domain, Token
  14. from desecapi.serializers import (
  15. AuthenticatedBasicUserActionSerializer,
  16. EmailPasswordSerializer,
  17. )
  18. class DynAuthenticationMixin:
  19. def authenticate_credentials(self, username, key):
  20. user, token = TokenAuthentication().authenticate_credentials(key)
  21. # Make sure username is not misleading
  22. try:
  23. if (
  24. username in ["", user.email]
  25. or Domain.objects.filter_qname(username.lower(), owner=user).exists()
  26. ):
  27. return user, token
  28. except ValueError:
  29. pass
  30. raise exceptions.AuthenticationFailed
  31. class TokenAuthentication(RestFrameworkTokenAuthentication):
  32. model = Token
  33. # Note: This method's runtime depends on in what way a credential is invalid (expired, wrong client IP).
  34. # It thus exposes the failure reason when under timing attack.
  35. def authenticate(self, request):
  36. try:
  37. user, token = super().authenticate(
  38. request
  39. ) # may raise exceptions.AuthenticationFailed if token is invalid
  40. except TypeError: # no token given
  41. return None # unauthenticated
  42. # REMOTE_ADDR is populated by the environment of the wsgi-request [1], which in turn is set up by nginx as per
  43. # uwsgi_params [2]. The value of $remote_addr finally is given by the network connection [3].
  44. # [1]: https://github.com/django/django/blob/stable/3.1.x/django/core/handlers/wsgi.py#L77
  45. # [2]: https://github.com/desec-io/desec-stack/blob/62820ad/www/conf/sites-available/90-desec.api.location.var#L11
  46. # [3]: https://nginx.org/en/docs/http/ngx_http_core_module.html#var_remote_addr
  47. # While the request.META dictionary contains a mixture of values from various sources, HTTP headers have keys
  48. # with the HTTP_ prefix. Client addresses can therefore not be spoofed through headers.
  49. # In case the stack is run behind an application proxy, the address will be the proxy's address. Extracting the
  50. # real client address is currently not supported. For further information on this case, see
  51. # https://www.django-rest-framework.org/api-guide/throttling/#how-clients-are-identified
  52. client_ip = ip_address(request.META.get("REMOTE_ADDR"))
  53. # This can likely be done within Postgres with django-postgres-extensions (client_ip <<= ANY allowed_subnets).
  54. # However, the django-postgres-extensions package is unmaintained, and the GitHub repo has been archived.
  55. if not any(client_ip in subnet for subnet in token.allowed_subnets):
  56. raise exceptions.AuthenticationFailed("Invalid token.")
  57. return user, token
  58. def authenticate_credentials(self, key):
  59. key = Token.make_hash(key)
  60. try:
  61. user, token = super().authenticate_credentials(key)
  62. except TypeError: # no token given
  63. return None # unauthenticated
  64. if not token.is_valid:
  65. raise exceptions.AuthenticationFailed("Invalid token.")
  66. token.last_used = timezone.now()
  67. token.save()
  68. return user, token
  69. class BasicTokenAuthentication(BaseAuthentication, DynAuthenticationMixin):
  70. """
  71. HTTP Basic authentication that uses username and token.
  72. Clients should authenticate by passing the username and the token as a
  73. password in the "Authorization" HTTP header, according to the HTTP
  74. Basic Authentication Scheme
  75. Authorization: Basic dXNlcm5hbWU6dG9rZW4=
  76. For username "username" and password "token".
  77. """
  78. # A custom token model may be used, but must have the following properties.
  79. #
  80. # * key -- The string identifying the token
  81. # * user -- The user to which the token belongs
  82. model = Token
  83. def authenticate(self, request):
  84. auth = get_authorization_header(request).split()
  85. if not auth or auth[0].lower() != b"basic":
  86. return None
  87. if len(auth) == 1:
  88. msg = "Invalid basic auth token header. No credentials provided."
  89. raise exceptions.AuthenticationFailed(msg)
  90. elif len(auth) > 2:
  91. msg = "Invalid basic auth token header. Basic authentication string should not contain spaces."
  92. raise exceptions.AuthenticationFailed(msg)
  93. try:
  94. username, key = (
  95. base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).split(":")
  96. )
  97. return self.authenticate_credentials(username, key)
  98. except Exception:
  99. raise exceptions.AuthenticationFailed("badauth")
  100. def authenticate_header(self, request):
  101. return "Basic"
  102. class URLParamAuthentication(BaseAuthentication, DynAuthenticationMixin):
  103. """
  104. Authentication against username/password as provided in URL parameters.
  105. """
  106. model = Token
  107. def authenticate(self, request):
  108. """
  109. Returns `(User, Token)` if a correct username and token have been supplied
  110. using URL parameters. Otherwise raises `AuthenticationFailed`.
  111. """
  112. if "username" not in request.query_params:
  113. msg = "No username URL parameter provided."
  114. raise exceptions.AuthenticationFailed(msg)
  115. if "password" not in request.query_params:
  116. msg = "No password URL parameter provided."
  117. raise exceptions.AuthenticationFailed(msg)
  118. try:
  119. return self.authenticate_credentials(
  120. request.query_params["username"], request.query_params["password"]
  121. )
  122. except Exception:
  123. raise exceptions.AuthenticationFailed("badauth")
  124. class EmailPasswordPayloadAuthentication(BaseAuthentication):
  125. authenticate_credentials = BasicAuthentication.authenticate_credentials
  126. def authenticate(self, request):
  127. serializer = EmailPasswordSerializer(data=request.data)
  128. serializer.is_valid(raise_exception=True)
  129. return self.authenticate_credentials(
  130. serializer.data["email"], serializer.data["password"], request
  131. )
  132. class AuthenticatedBasicUserActionAuthentication(BaseAuthentication):
  133. """
  134. Authenticates a request based on whether the serializer determines the validity of the given verification code
  135. based on the view's 'code' kwarg and the view serializer's code validity period.
  136. """
  137. def authenticate(self, request):
  138. view = request.parser_context["view"]
  139. return self.authenticate_credentials(view.get_serializer_context())
  140. def authenticate_credentials(self, context):
  141. serializer = AuthenticatedBasicUserActionSerializer(data={}, context=context)
  142. serializer.is_valid(raise_exception=True)
  143. user = serializer.validated_data["user"]
  144. email_verified = datetime.datetime.fromtimestamp(
  145. serializer.timestamp, datetime.timezone.utc
  146. )
  147. user.email_verified = max(user.email_verified or email_verified, email_verified)
  148. user.save()
  149. # When user.is_active is None, activation is pending. We need to admit them to finish activation, so only
  150. # reject strictly False. There are permissions to make sure that such accounts can't do anything else.
  151. if user.is_active == False:
  152. raise exceptions.AuthenticationFailed("User inactive.")
  153. return user, None
  154. class TokenHasher(PBKDF2PasswordHasher):
  155. algorithm = "pbkdf2_sha256_iter1"
  156. iterations = 1