Change password: change to require a signed request, just like login.

Without this, it would be sufficient to steal an auth token to render the account
unusable because it would be possible to just reset the encrypted content
of the account. With this change we require the user to actually know
the account password in order to do it.
master
Tom Hacohen 4 years ago
parent 54268ac027
commit ab0d85c84f

@ -425,7 +425,7 @@ class AuthenticationLoginInnerSerializer(AuthenticationLoginChallengeSerializer)
raise NotImplementedError() raise NotImplementedError()
class AuthenticationChangePasswordSerializer(serializers.ModelSerializer): class AuthenticationChangePasswordInnerSerializer(AuthenticationLoginInnerSerializer):
loginPubkey = BinaryBase64Field() loginPubkey = BinaryBase64Field()
encryptedContent = BinaryBase64Field() encryptedContent = BinaryBase64Field()

@ -49,7 +49,7 @@ from .models import (
) )
from .serializers import ( from .serializers import (
b64encode, b64encode,
AuthenticationChangePasswordSerializer, AuthenticationChangePasswordInnerSerializer,
AuthenticationSignupSerializer, AuthenticationSignupSerializer,
AuthenticationLoginChallengeSerializer, AuthenticationLoginChallengeSerializer,
AuthenticationLoginSerializer, AuthenticationLoginSerializer,
@ -562,6 +562,44 @@ class AuthenticationViewSet(viewsets.ViewSet):
kwargs = {User.USERNAME_FIELD: username} kwargs = {User.USERNAME_FIELD: username}
return get_object_or_404(self.get_queryset(), **kwargs) return get_object_or_404(self.get_queryset(), **kwargs)
def validate_login_request(self, request, validated_data, response_raw, signature, expected_action):
from datetime import datetime
username = validated_data.get('username')
user = self.get_login_user(username)
host = validated_data['host']
challenge = validated_data['challenge']
action = validated_data['action']
salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt)
box = nacl.secret.SecretBox(enc_key)
challenge_data = json.loads(box.decrypt(challenge).decode())
now = int(datetime.now().timestamp())
if action != expected_action:
content = {'code': 'wrong_action', 'detail': 'Expected "{}" but got something else'.format(expected_action)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS:
content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif challenge_data['userId'] != user.id:
content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif not settings.DEBUG and host != request.get_host():
detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host())
content = {'code': 'wrong_host', 'detail': detail}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder)
try:
verify_key.verify(response_raw, signature)
except nacl.exceptions.BadSignatureError:
return Response({'code': 'login_bad_signature'}, status=status.HTTP_400_BAD_REQUEST)
return None
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=['POST'])
def login_challenge(self, request): def login_challenge(self, request):
from datetime import datetime from datetime import datetime
@ -593,48 +631,23 @@ class AuthenticationViewSet(viewsets.ViewSet):
@action_decorator(detail=False, methods=['POST']) @action_decorator(detail=False, methods=['POST'])
def login(self, request): def login(self, request):
from datetime import datetime
outer_serializer = AuthenticationLoginSerializer(data=request.data) outer_serializer = AuthenticationLoginSerializer(data=request.data)
if outer_serializer.is_valid(): outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response'] response_raw = outer_serializer.validated_data['response']
response = json.loads(response_raw.decode()) response = json.loads(response_raw.decode())
signature = outer_serializer.validated_data['signature'] signature = outer_serializer.validated_data['signature']
serializer = AuthenticationLoginInnerSerializer(data=response, context={'host': request.get_host()}) serializer = AuthenticationLoginInnerSerializer(data=response, context={'host': request.get_host()})
if serializer.is_valid(): serializer.is_valid(raise_exception=True)
username = serializer.validated_data.get('username')
user = self.get_login_user(username)
host = serializer.validated_data['host']
challenge = serializer.validated_data['challenge']
action = serializer.validated_data['action']
salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt)
box = nacl.secret.SecretBox(enc_key)
challenge_data = json.loads(box.decrypt(challenge).decode())
now = int(datetime.now().timestamp())
if action != "login":
content = {'code': 'wrong_action', 'detail': 'Expected "login" but got something else'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS:
content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif challenge_data['userId'] != user.id:
content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif not settings.DEBUG and host != request.get_host():
detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host())
content = {'code': 'wrong_host', 'detail': detail}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder) bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "login")
if bad_login_response is not None:
return bad_login_response
try: username = serializer.validated_data.get('username')
verify_key.verify(response_raw, signature) user = self.get_login_user(username)
except nacl.exceptions.BadSignatureError:
return Response({'code': 'login_bad_signature'}, status=status.HTTP_400_BAD_REQUEST)
data = self.login_response_data(user) data = self.login_response_data(user)
@ -642,8 +655,6 @@ class AuthenticationViewSet(viewsets.ViewSet):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) @action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes)
def logout(self, request): def logout(self, request):
request.auth.delete() request.auth.delete()
@ -652,11 +663,25 @@ class AuthenticationViewSet(viewsets.ViewSet):
@action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes) @action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes)
def change_password(self, request): def change_password(self, request):
serializer = AuthenticationChangePasswordSerializer(request.user.userinfo, data=request.data) outer_serializer = AuthenticationLoginSerializer(data=request.data)
outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response']
response = json.loads(response_raw.decode())
signature = outer_serializer.validated_data['signature']
serializer = AuthenticationChangePasswordInnerSerializer(
request.user.userinfo, data=response, context={'host': request.get_host()})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "changePassword")
if bad_login_response is not None:
return bad_login_response
serializer.save() serializer.save()
return Response(status=status.HTTP_200_OK) return Response({}, status=status.HTTP_200_OK)
class TestAuthenticationViewSet(viewsets.ViewSet): class TestAuthenticationViewSet(viewsets.ViewSet):

Loading…
Cancel
Save