Repository: getsentry/sentry

Issue:
2FA cannot *require* a password
We need to check `user.has_usable_password()` before requiring password confirmation, as many accounts will not have one (e.g. when they're created with SSO).



Discussion:
@macqueen 

An example of how this is done ATM is in the `AccountSettingsForm`


Resolving patch:
diff --git a/src/sentry/web/forms/accounts.py b/src/sentry/web/forms/accounts.py
--- a/src/sentry/web/forms/accounts.py
+++ b/src/sentry/web/forms/accounts.py
@@ -531,12 +531,6 @@ class TwoFactorForm(forms.Form):
                    'autofocus': True,
         }),
     )
-    password = forms.CharField(
-        label=_('Sentry account password'),
-        widget=forms.PasswordInput(),
-        help_text='You will need to enter your current Sentry account password to make changes.',
-        required=True,
-    )
 
 
 class ConfirmPasswordForm(forms.Form):
@@ -546,3 +540,12 @@ class ConfirmPasswordForm(forms.Form):
         help_text='You will need to enter your current Sentry account password to make changes.',
         required=True,
     )
+
+    def __init__(self, user, *args, **kwargs):
+        self.user = user
+        super(ConfirmPasswordForm, self).__init__(*args, **kwargs)
+
+        needs_password = user.has_usable_password()
+
+        if not needs_password:
+            del self.fields['password']
diff --git a/src/sentry/web/frontend/accounts_twofactor.py b/src/sentry/web/frontend/accounts_twofactor.py
--- a/src/sentry/web/frontend/accounts_twofactor.py
+++ b/src/sentry/web/frontend/accounts_twofactor.py
@@ -78,19 +78,23 @@ def delete_authenticator(self, interface):
                 iface.authenticator.delete()
 
     def remove(self, request, interface):
-        form = ConfirmPasswordForm
+        form = ConfirmPasswordForm(request.user)
 
         if 'no' in request.POST or \
            not interface.is_enrolled:
             return HttpResponseRedirect(reverse('sentry-account-settings-2fa'))
         elif 'yes' in request.POST:
-            form = ConfirmPasswordForm(request.POST)
-            if form.is_valid():
-                if request.user.check_password(form.cleaned_data['password']):
-                    self.delete_authenticator(interface)
-                    return HttpResponseRedirect(reverse('sentry-account-settings-2fa'))
-                else:
-                    form.errors['__all__'] = ['Invalid password.']
+            form = ConfirmPasswordForm(request.user, request.POST)
+            if 'password' in form.fields:
+                if form.is_valid():
+                    if request.user.check_password(form.cleaned_data['password']):
+                        self.delete_authenticator(interface)
+                        return HttpResponseRedirect(reverse('sentry-account-settings-2fa'))
+                    else:
+                        form.errors['__all__'] = ['Invalid password.']
+            else:
+                self.delete_authenticator(interface)
+                return HttpResponseRedirect(reverse('sentry-account-settings-2fa'))
 
         all_interfaces = Authenticator.objects.all_interfaces_for_user(
             request.user)
@@ -159,8 +163,9 @@ def enroll(self, request, interface, insecure=False):
 
         if 'otp' in request.POST:
             form = TwoFactorForm(request.POST)
-            if form.is_valid():
-                if request.user.check_password(form.cleaned_data['password']):
+            password_form = ConfirmPasswordForm(request.user, request.POST)
+            if 'password' in password_form.fields and password_form.is_valid():
+                if request.user.check_password(password_form.cleaned_data['password']):
                     if form.is_valid() and interface.validate_otp(
                             form.cleaned_data['otp']):
                         return TwoFactorSettingsView.enroll(self, request, interface)
@@ -168,11 +173,20 @@ def enroll(self, request, interface, insecure=False):
                         form.errors['__all__'] = ['Invalid confirmation code.']
                 else:
                     form.errors['__all__'] = ['Invalid password.']
+            else:
+                if form.is_valid() and interface.validate_otp(
+                        form.cleaned_data['otp']):
+                    return TwoFactorSettingsView.enroll(self, request, interface)
+                else:
+                    form.errors['__all__'] = ['Invalid confirmation code.']
+
         else:
             form = TwoFactorForm()
+            password_form = ConfirmPasswordForm(request.user)
 
         context = self.make_context(request, interface)
         context['otp_form'] = form
+        context['password_form'] = password_form
         context['provision_qrcode'] = interface.get_provision_qrcode(
             request.user.email)
         return render_to_response('sentry/account/twofactor/enroll_totp.html',


Test patch: