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.
This commit is contained in:
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
username = serializer.validated_data.get('username')
|
username = serializer.validated_data.get('username')
|
||||||
user = self.get_login_user(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)
|
|
||||||
|
|
||||||
try:
|
|
||||||
verify_key.verify(response_raw, signature)
|
|
||||||
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…
Reference in New Issue
Block a user