authentication.py 7.1 KB

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