def save(self, commit=True): if self.cleaned_data.get('new_password'): self.user.set_password(self.cleaned_data['new_password']) self.user.refresh_session_nonce(self.request) capture_security_activity( account=self.user, type='password-changed', actor=self.request.user, ip_address=self.request.META['REMOTE_ADDR'], send_email=True, ) self.user.name = self.cleaned_data['name'] if self.cleaned_data['email'] != self.user.email: new_username = self.user.email == self.user.username else: new_username = False self.user.email = self.cleaned_data['email'] if self.cleaned_data.get('username'): self.user.username = self.cleaned_data['username'] elif new_username and not User.objects.filter(username__iexact=self.user.email).exists(): self.user.username = self.user.email if commit: self.user.save() return self.user
def put(self, request, user): # pass some context to serializer otherwise when we create a new serializer instance, # user.password gets set to new plaintext password from request and # `user.has_usable_password` becomes False serializer = UserPasswordSerializer(user, data=request.DATA, context={ 'is_managed': user.is_managed, 'has_usable_password': user.has_usable_password(), }) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) result = serializer.object user.set_password(result.passwordNew) user.refresh_session_nonce(request._request) user.clear_lost_passwords() user = serializer.save() capture_security_activity( account=user, type='password-changed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], send_email=True, ) return Response(status=status.HTTP_204_NO_CONTENT)
def put(self, request, user, auth_id): """ Modify authenticator interface `````````````````````````````` Currently, only supports regenerating recovery codes :pparam string user_id: user id or 'me' for current user :pparam int auth_id: authenticator model id :auth required: """ try: authenticator = Authenticator.objects.get(user=user, id=auth_id) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) interface = authenticator.interface if interface.interface_id == "recovery": interface.regenerate_codes() capture_security_activity( account=user, type="recovery-codes-regenerated", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={"authenticator": authenticator}, send_email=True, ) return Response(serialize(interface))
def configure(self, request, interface): # Try to remove a key handle. If this returns `False` it means we # are about to remove the last key handle. In that case just # bubble through to the configure page which will pick up the # 'remove' in the form and bring up the remove screen for the # entire authentication method. key_handle = request.POST.get('key_handle') if key_handle: device_name = interface.get_device_name(key_handle) if 'remove' in request.POST and interface.remove_u2f_device( key_handle): interface.authenticator.save() capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, 'device_name': device_name }, send_email=True, ) return HttpResponseRedirect(request.path) return TwoFactorSettingsView.configure(self, request, interface)
def put(self, request: Request, user) -> Response: # pass some context to serializer otherwise when we create a new serializer instance, # user.password gets set to new plaintext password from request and # `user.has_usable_password` becomes False serializer = UserPasswordSerializer(data=request.data, context={"user": user}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) result = serializer.validated_data user.set_password(result["passwordNew"]) user.refresh_session_nonce(request._request) user.clear_lost_passwords() user.save() capture_security_activity( account=user, type="password-changed", actor=request.user, ip_address=request.META["REMOTE_ADDR"], send_email=True, ) return Response(status=status.HTTP_204_NO_CONTENT)
def enroll(self, request, interface, insecure=False): next = request.path # Only enroll if it's either not an insecure enrollment or we are # enrolling a backup interface when we already had a primary one. if not insecure \ or (interface.is_backup_interface and Authenticator.objects.user_has_2fa(request.user)): try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: # This can happen in some cases when races occur. We have # seen this when people press the submit button twice. In # that case just go to the overview page of 2fa next = reverse('sentry-account-settings-2fa') else: capture_security_activity( account=request.user, type='mfa-added', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=False, ) request.user.refresh_session_nonce(self.request) request.user.save() if Authenticator.objects.auto_add_recovery_codes(request.user): next = reverse('sentry-account-settings-2fa-recovery') return HttpResponseRedirect(next)
def enroll(self, request, interface, insecure=False): next = request.path # Only enroll if it's either not an insecure enrollment or we are # enrolling a backup interface when we already had a primary one. if not insecure \ or (interface.is_backup_interface and Authenticator.objects.user_has_2fa(request.user)): try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: # This can happen in some cases when races occur. We have # seen this when people press the submit button twice. In # that case just go to the overview page of 2fa next = reverse('sentry-account-settings-2fa') else: capture_security_activity( account=request.user, type='mfa-added', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=True, ) request.user.refresh_session_nonce(self.request) request.user.save() if Authenticator.objects.auto_add_recovery_codes(request.user): next = reverse('sentry-account-settings-2fa-recovery') return HttpResponseRedirect(next)
def delete(self, request, user, auth_id): try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except Authenticator.DoesNotExist: return Response(status=404) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. if not authenticator.interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user(user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, ) capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=not authenticator.interface.is_backup_interface, ) return Response(status=204)
def delete(self, request, user, auth_id): try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except Authenticator.DoesNotExist: return Response(status=404) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. if not authenticator.interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user( user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, ) capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=not authenticator.interface.is_backup_interface, ) return Response(status=204)
def recover_confirm(request, user_id, hash): try: password_hash = LostPasswordHash.objects.get(user=user_id, hash=hash) if not password_hash.is_valid(): password_hash.delete() raise LostPasswordHash.DoesNotExist user = password_hash.user except LostPasswordHash.DoesNotExist: context = {} tpl = 'sentry/account/recover/failure.html' else: tpl = 'sentry/account/recover/confirm.html' if request.method == 'POST': form = ChangePasswordRecoverForm(request.POST) if form.is_valid(): with transaction.atomic(): user.set_password(form.cleaned_data['password']) user.refresh_session_nonce(request) user.save() # Ugly way of doing this, but Django requires the backend be set user = authenticate( username=user.username, password=form.cleaned_data['password'], ) login_user(request, user) password_hash.delete() capture_security_activity( account=user, type='password-changed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], send_email=True, ) return login_redirect(request) else: form = ChangePasswordRecoverForm() context = { 'form': form, } return render_to_response(tpl, context, request)
def recover_confirm(request, user_id, hash, mode='recover'): try: password_hash = LostPasswordHash.objects.get(user=user_id, hash=hash) if not password_hash.is_valid(): password_hash.delete() raise LostPasswordHash.DoesNotExist user = password_hash.user except LostPasswordHash.DoesNotExist: tpl = get_template('failure', mode) return render_to_response(tpl, {}, request) if request.method == 'POST': form = ChangePasswordRecoverForm(request.POST) if form.is_valid(): with transaction.atomic(): user.set_password(form.cleaned_data['password']) user.refresh_session_nonce(request) user.save() # Ugly way of doing this, but Django requires the backend be set user = authenticate( username=user.username, password=form.cleaned_data['password'], ) # Only log the user in if there is no two-factor on the # account. if not Authenticator.objects.user_has_2fa(user): login_user(request, user) password_hash.delete() capture_security_activity( account=user, type='password-changed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], send_email=True, ) return login_redirect(request) else: form = ChangePasswordRecoverForm() tpl = get_template('confirm', mode) context = {'form': form} return render_to_response(tpl, context, request)
def _regenerate_recovery_code(self, authenticator, request, user): interface = authenticator.interface if interface.interface_id == "recovery": interface.regenerate_codes() capture_security_activity( account=user, type="recovery-codes-regenerated", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={"authenticator": authenticator}, send_email=True, ) return Response(serialize(interface))
def configure(self, request, interface): if 'regenerate' in request.POST: interface.regenerate_codes() capture_security_activity( account=request.user, type='recovery-codes-regenerated', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=True, ) return HttpResponseRedirect(request.path) return TwoFactorSettingsView.configure(self, request, interface)
def recover_confirm(request, user_id, hash, mode="recover"): try: password_hash = LostPasswordHash.objects.get(user=user_id, hash=hash) if not password_hash.is_valid(): password_hash.delete() raise LostPasswordHash.DoesNotExist user = password_hash.user except LostPasswordHash.DoesNotExist: return render_to_response( u"sentry/account/{}/{}.html".format(mode, "failure"), {}, request) if request.method == "POST": form = ChangePasswordRecoverForm(request.POST) if form.is_valid(): with transaction.atomic(): user.set_password(form.cleaned_data["password"]) user.refresh_session_nonce(request) user.save() # Ugly way of doing this, but Django requires the backend be set user = authenticate(username=user.username, password=form.cleaned_data["password"]) # Only log the user in if there is no two-factor on the # account. if not Authenticator.objects.user_has_2fa(user): login_user(request, user) password_hash.delete() capture_security_activity( account=user, type="password-changed", actor=request.user, ip_address=request.META["REMOTE_ADDR"], send_email=True, ) return login_redirect(request) else: form = ChangePasswordRecoverForm() return render_to_response( u"sentry/account/{}/{}.html".format(mode, "confirm"), {"form": form}, request)
def delete_authenticator(self, request, interface): if interface.authenticator is None: return with transaction.atomic(): user = interface.authenticator.user capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=True, ) interface.authenticator.delete() # If this was an authenticator that was a backup interface we just # deleted, then nothing happens. if interface.is_backup_interface: return # If however if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. interfaces = Authenticator.objects.all_interfaces_for_user(user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, )
def delete_authenticator(self, request, interface): if interface.authenticator is None: return with transaction.atomic(): user = interface.authenticator.user capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=True, ) interface.authenticator.delete() # If this was an authenticator that was a backup interface we just # deleted, then nothing happens. if interface.is_backup_interface: return # If however if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. interfaces = Authenticator.objects.all_interfaces_for_user(user) backup_interfaces = [x for x in interfaces if x.is_backup_interface] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, )
def post(self, request: Request) -> Response: serializer = ApiTokenSerializer(data=request.data) if serializer.is_valid(): result = serializer.validated_data token = ApiToken.objects.create( user=request.user, scope_list=result["scopes"], refresh_token=None, expires_at=None ) capture_security_activity( account=request.user, type="api-token-generated", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={}, send_email=True, ) return Response(serialize(token, request.user), status=201) return Response(serializer.errors, status=400)
def post(self, request): serializer = ApiTokenSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object token = ApiToken.objects.create( user=request.user, scope_list=result['scopes'], refresh_token=None, expires_at=None, ) capture_security_activity(account=request.user, type='api-token-generated', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={}, send_email=True) return Response(serialize(token, request.user), status=201) return Response(serializer.errors, status=400)
def put(self, request, user, auth_id): """ Modify authenticator interface `````````````````````````````` Currently, only supports regenerating recovery codes :pparam string user_id: user id or 'me' for current user :pparam int auth_id: authenticator model id :auth required: """ try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) interface = authenticator.interface if interface.interface_id == 'recovery': interface.regenerate_codes() capture_security_activity( account=user, type='recovery-codes-regenerated', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=True ) return Response(serialize(interface))
def post(self, request): serializer = ApiTokenSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object token = ApiToken.objects.create( user=request.user, scope_list=result['scopes'], refresh_token=None, expires_at=None, ) capture_security_activity( account=request.user, type='api-token-generated', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={}, send_email=True ) return Response(serialize(token, request.user), status=201) return Response(serializer.errors, status=400)
def delete(self, request, user, auth_id): """ Remove authenticator ```````````````````` :pparam string user_id: user id or 'me' for current user :pparam string auth_id: authenticator model id :auth required: """ try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. if not authenticator.interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user( user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, ) capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=not authenticator.interface.is_backup_interface, ) return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, user, auth_id, interface_device_id=None): """ Remove authenticator ```````````````````` :pparam string user_id: user id or 'me' for current user :pparam string auth_id: authenticator model id :pparam string interface_device_id: some interfaces (u2f) allow multiple devices :auth required: """ try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) interface = authenticator.interface # Remove a single device and not entire authentication method if interface.interface_id == 'u2f' and interface_device_id is not None: device_name = interface.get_device_name(interface_device_id) # Can't remove if this is the last device, will return False if so if not interface.remove_u2f_device(interface_device_id): return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) interface.authenticator.save() capture_security_activity(account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, 'device_name': device_name }, send_email=True) return Response(status=status.HTTP_204_NO_CONTENT) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. if not interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user( user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, ) capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=not interface.is_backup_interface, ) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, user, interface_id): """ Enroll in authenticator interface ````````````````````````````````` :pparam string user_id: user id or "me" for current user :pparam string interface_id: interface id :auth: required """ # Using `request.user` here because superuser should not be able to set a user's 2fa # start activation serializer_cls = serializer_map.get(interface_id, None) if serializer_cls is None: return Response(status=status.HTTP_404_NOT_FOUND) serializer = serializer_cls(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) interface = Authenticator.objects.get_interface(request.user, interface_id) # Not all interfaces allow multi enrollment # # This is probably un-needed because we catch # `Authenticator.AlreadyEnrolled` when attempting to enroll if interface.is_enrolled and not interface.allow_multi_enrollment: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) try: interface.secret = request.DATA['secret'] except KeyError: pass # Need to update interface with phone number before validating OTP if 'phone' in request.DATA: interface.phone_number = serializer.data['phone'] # Disregarding value of 'otp', if no OTP was provided, # send text message to phone number with OTP if 'otp' not in request.DATA: if interface.send_text(for_enrollment=True, request=request._request): return Response(status=status.HTTP_204_NO_CONTENT) else: # Error sending text message return Response(SEND_SMS_ERR, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Attempt to validate OTP if 'otp' in request.DATA and not interface.validate_otp(serializer.data['otp']): return Response(INVALID_OTP_ERR, status=status.HTTP_400_BAD_REQUEST) # Try u2f enrollment if interface_id == 'u2f': # What happens when this fails? interface.try_enroll( serializer.data['challenge'], serializer.data['response'], serializer.data['deviceName'] ) try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) else: capture_security_activity( account=request.user, type='mfa-added', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': interface.authenticator, }, send_email=False, ) request.user.clear_lost_passwords() request.user.refresh_session_nonce(self.request) request.user.save() Authenticator.objects.auto_add_recovery_codes(request.user) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, user, interface_id): """ Enroll in authenticator interface ````````````````````````````````` :pparam string user_id: user id or "me" for current user :pparam string interface_id: interface id :auth: required """ # Using `request.user` here because superuser should not be able to set a user's 2fa # start activation serializer_cls = serializer_map.get(interface_id, None) if serializer_cls is None: return Response(status=status.HTTP_404_NOT_FOUND) serializer = serializer_cls(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) interface = Authenticator.objects.get_interface( request.user, interface_id) # Not all interfaces allow multi enrollment # # This is probably un-needed because we catch # `Authenticator.AlreadyEnrolled` when attempting to enroll if interface.is_enrolled and not interface.allow_multi_enrollment: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) try: interface.secret = request.data["secret"] except KeyError: pass context = {} # Need to update interface with phone number before validating OTP if "phone" in request.data: interface.phone_number = serializer.data["phone"] # Disregarding value of 'otp', if no OTP was provided, # send text message to phone number with OTP if "otp" not in request.data: if interface.send_text(for_enrollment=True, request=request._request): return Response(status=status.HTTP_204_NO_CONTENT) else: # Error sending text message return Response( SEND_SMS_ERR, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Attempt to validate OTP if "otp" in request.data and not interface.validate_otp( serializer.data["otp"]): return Response(INVALID_OTP_ERR, status=status.HTTP_400_BAD_REQUEST) # Try u2f enrollment if interface_id == "u2f": # What happens when this fails? interface.try_enroll( serializer.data["challenge"], serializer.data["response"], serializer.data["deviceName"], ) context.update({"device_name": serializer.data["deviceName"]}) try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) else: context.update({"authenticator": interface.authenticator}) capture_security_activity( account=request.user, type="mfa-added", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context=context, send_email=True, ) request.user.clear_lost_passwords() request.user.refresh_session_nonce(self.request) request.user.save() Authenticator.objects.auto_add_recovery_codes(request.user) # Try to accept an org invite pending 2FA enrollment member_id = serializer.data.get("memberId") token = serializer.data.get("token") if member_id and token: try: helper = ApiInviteHelper( instance=self, request=request, member_id=member_id, token=token, logger=logger, ) except OrganizationMember.DoesNotExist: logger.error("Failed to accept pending org invite", exc_info=True) else: if helper.valid_request(): helper.accept_invite() response = Response(status=status.HTTP_204_NO_CONTENT) helper.remove_invite_cookie(response) return response return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request: Request, user, auth_id, interface_device_id=None) -> Response: """ Remove authenticator ```````````````````` :pparam string user_id: user id or 'me' for current user :pparam string auth_id: authenticator model id :pparam string interface_device_id: some interfaces (u2f) allow multiple devices :auth required: """ try: authenticator = Authenticator.objects.get(user=user, id=auth_id) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) interface = authenticator.interface # Remove a single device and not entire authentication method if interface.interface_id == "u2f" and interface_device_id is not None: device_name = interface.get_device_name(interface_device_id) # Can't remove if this is the last device, will return False if so if not interface.remove_u2f_device(interface_device_id): return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) interface.authenticator.save() capture_security_activity( account=user, type="mfa-removed", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={ "authenticator": authenticator, "device_name": device_name }, send_email=True, ) return Response(status=status.HTTP_204_NO_CONTENT) if not is_active_superuser(request): # if the user's organization requires 2fa, # don't delete the last auth method enrolled_methods = Authenticator.objects.all_interfaces_for_user( user, ignore_backup=True) last_2fa_method = len(enrolled_methods) == 1 require_2fa = user.get_orgs_require_2fa().exists() if require_2fa and last_2fa_method: return Response( { "detail": "Cannot delete authenticator because organization requires 2FA" }, status=status.HTTP_403_FORBIDDEN, ) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remains are backup interfaces, then we kill them in the # process. if not interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user( user) backup_interfaces = [ x for x in interfaces if x.is_backup_interface ] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type="mfa-removed", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={"authenticator": iface.authenticator}, send_email=False, ) capture_security_activity( account=user, type="mfa-removed", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context={"authenticator": authenticator}, send_email=not interface.is_backup_interface, ) return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, user, auth_id, interface_device_id=None): """ Remove authenticator ```````````````````` :pparam string user_id: user id or 'me' for current user :pparam string auth_id: authenticator model id :pparam string interface_device_id: some interfaces (u2f) allow multiple devices :auth required: """ try: authenticator = Authenticator.objects.get( user=user, id=auth_id, ) except (ValueError, Authenticator.DoesNotExist): return Response(status=status.HTTP_404_NOT_FOUND) interface = authenticator.interface # Remove a single device and not entire authentication method if interface.interface_id == 'u2f' and interface_device_id is not None: device_name = interface.get_device_name(interface_device_id) # Can't remove if this is the last device, will return False if so if not interface.remove_u2f_device(interface_device_id): return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) interface.authenticator.save() capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, 'device_name': device_name }, send_email=True ) return Response(status=status.HTTP_204_NO_CONTENT) with transaction.atomic(): authenticator.delete() # if we delete an actual authenticator and all that # remainds are backup interfaces, then we kill them in the # process. if not interface.is_backup_interface: interfaces = Authenticator.objects.all_interfaces_for_user(user) backup_interfaces = [x for x in interfaces if x.is_backup_interface] if len(backup_interfaces) == len(interfaces): for iface in backup_interfaces: iface.authenticator.delete() # wait to generate entries until all pending writes # have been sent to db for iface in backup_interfaces: capture_security_activity( account=request.user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': iface.authenticator, }, send_email=False, ) capture_security_activity( account=user, type='mfa-removed', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context={ 'authenticator': authenticator, }, send_email=not interface.is_backup_interface, ) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, user, interface_id): """ Enroll in authenticator interface ````````````````````````````````` :pparam string user_id: user id or "me" for current user :pparam string interface_id: interface id :auth: required """ # Using `request.user` here because superuser should not be able to set a user's 2fa # start activation serializer_cls = serializer_map.get(interface_id, None) if serializer_cls is None: return Response(status=status.HTTP_404_NOT_FOUND) serializer = serializer_cls(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) interface = Authenticator.objects.get_interface(request.user, interface_id) # Not all interfaces allow multi enrollment # # This is probably un-needed because we catch # `Authenticator.AlreadyEnrolled` when attempting to enroll if interface.is_enrolled and not interface.allow_multi_enrollment: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) try: interface.secret = request.DATA['secret'] except KeyError: pass context = {} # Need to update interface with phone number before validating OTP if 'phone' in request.DATA: interface.phone_number = serializer.data['phone'] # Disregarding value of 'otp', if no OTP was provided, # send text message to phone number with OTP if 'otp' not in request.DATA: if interface.send_text(for_enrollment=True, request=request._request): return Response(status=status.HTTP_204_NO_CONTENT) else: # Error sending text message return Response(SEND_SMS_ERR, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Attempt to validate OTP if 'otp' in request.DATA and not interface.validate_otp(serializer.data['otp']): return Response(INVALID_OTP_ERR, status=status.HTTP_400_BAD_REQUEST) # Try u2f enrollment if interface_id == 'u2f': # What happens when this fails? interface.try_enroll( serializer.data['challenge'], serializer.data['response'], serializer.data['deviceName'] ) context.update({ 'device_name': serializer.data['deviceName'] }) try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) else: context.update({ 'authenticator': interface.authenticator }) capture_security_activity( account=request.user, type='mfa-added', actor=request.user, ip_address=request.META['REMOTE_ADDR'], context=context, send_email=True, ) request.user.clear_lost_passwords() request.user.refresh_session_nonce(self.request) request.user.save() Authenticator.objects.auto_add_recovery_codes(request.user) return Response(status=status.HTTP_204_NO_CONTENT)