Просмотр исходного кода

feat(u2f): deprecated u2f-api for sign-in flow - BE (#30612)

Richard Ma 3 лет назад
Родитель
Сommit
98cdba27ed

+ 1 - 0
requirements-base.txt

@@ -40,6 +40,7 @@ PyJWT==2.1.0
 python-dateutil==2.8.1
 python-memcached==1.59
 python-u2flib-server==5.0.0
+fido2==0.9.2
 python3-saml==1.10.1
 PyYAML==5.4
 rb==1.9.0

+ 34 - 2
src/sentry/auth/authenticators/u2f.py

@@ -3,6 +3,10 @@ from time import time
 from cryptography.exceptions import InvalidKey, InvalidSignature
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
+from fido2.client import ClientData
+from fido2.ctap2 import AuthenticatorData, base
+from fido2.server import U2FFido2Server
+from fido2.utils import websafe_decode
 from u2flib_server import u2f
 from u2flib_server.model import DeviceRegistration
 
@@ -99,9 +103,11 @@ class U2fInterface(AuthenticatorInterface):
 
     def activate(self, request):
         challenge = dict(u2f.begin_authentication(self.u2f_app_id, self.get_u2f_devices()))
+
         # XXX: Upgrading python-u2flib-server to 5.0.0 changes the response
         # format. Our current js u2f library expects the old format, so
         # massaging the data to include the old `authenticateRequests` key here.
+
         authenticate_requests = []
         for registered_key in challenge["registeredKeys"]:
             authenticate_requests.append(
@@ -116,9 +122,35 @@ class U2fInterface(AuthenticatorInterface):
 
         return ActivationChallengeResult(challenge=challenge)
 
-    def validate_response(self, request, challenge, response):
+    def validate_response(self, request, challenge, response, is_webauthn_signin_ff_enabled):
         try:
-            u2f.complete_authentication(challenge, response, self.u2f_facets)
+            if not is_webauthn_signin_ff_enabled:
+                u2f.complete_authentication(challenge, response, self.u2f_facets)
+                return True
+            # TODO change rp.id later when register is implemented
+            server = U2FFido2Server(
+                app_id=challenge["appId"],
+                rp={"id": challenge["appId"], "name": "Sentry"},
+            )
+            state = {
+                "challenge": challenge["challenge"],
+                "user_verification": None,
+            }
+            credentials = []
+            for registeredKey in challenge["registeredKeys"]:
+                c = base.AttestedCredentialData.from_ctap1(
+                    websafe_decode(registeredKey["keyHandle"]),
+                    websafe_decode(registeredKey["publicKey"]),
+                )
+                credentials.append(c)
+            server.authenticate_complete(
+                state=state,
+                credentials=credentials,
+                credential_id=websafe_decode(response["keyHandle"]),
+                client_data=ClientData(websafe_decode(response["clientData"])),
+                auth_data=AuthenticatorData(websafe_decode(response["authenticatorData"])),
+                signature=websafe_decode(response["signatureData"]),
+            )
         except (InvalidSignature, InvalidKey, StopIteration):
             return False
         return True

+ 4 - 0
src/sentry/conf/server.py

@@ -1095,6 +1095,10 @@ SENTRY_FEATURES = {
     "organizations:issue-percent-display": False,
     # Enable team insights page
     "organizations:team-insights": True,
+    # Enable login with WebAuthn
+    "organizations:webauthn-login": False,
+    # Enable registering new key with WebAuthn
+    "organizations:webauthn-register": False,
     # Adds additional filters and a new section to issue alert rules.
     "projects:alert-filters": True,
     # Enable functionality to specify custom inbound filters on events.

+ 2 - 0
src/sentry/features/__init__.py

@@ -155,6 +155,8 @@ default_manager.add("organizations:transaction-events", OrganizationFeature, Tru
 default_manager.add("organizations:transaction-metrics-extraction", OrganizationFeature, True)
 default_manager.add("organizations:unhandled-issue-flag", OrganizationFeature)
 default_manager.add("organizations:unified-span-view", OrganizationFeature, True)
+default_manager.add("organizations:webauthn-login", OrganizationFeature, True)
+default_manager.add("organizations:webauthn-register", OrganizationFeature, True)
 default_manager.add("organizations:weekly-report-debugging", OrganizationFeature, True)
 default_manager.add("organizations:widget-library", OrganizationFeature, True)
 

+ 15 - 3
src/sentry/web/frontend/twofactor.py

@@ -3,7 +3,7 @@ import time
 from django.http import HttpResponse, HttpResponseRedirect
 from django.utils.translation import ugettext as _
 
-from sentry import options
+from sentry import features, options
 from sentry.app import ratelimiter
 from sentry.models import Authenticator
 from sentry.utils import auth, json
@@ -18,6 +18,14 @@ COOKIE_MAX_AGE = 60 * 60 * 24 * 31
 class TwoFactorAuthView(BaseView):
     auth_required = False
 
+    def _is_webauthn_signin_ff_enabled(self, user, request_user):
+        orgs = user.get_orgs()
+        if any(
+            features.has("organizations:webauthn-login", org, actor=request_user) for org in orgs
+        ):
+            return True
+        return False
+
     def perform_signin(self, request, user, interface=None):
         assert auth.login(request, user, passed_2fa=True)
         rv = HttpResponseRedirect(auth.get_login_redirect(request))
@@ -142,12 +150,15 @@ class TwoFactorAuthView(BaseView):
                 return self.perform_signin(request, user, used_interface)
             self.fail_signin(request, user, form)
 
-        # If a challenge and response exists, validate
+        # check if webauthn-login feature flag is enabled for frontend
+        webauthn_signin_ff = self._is_webauthn_signin_ff_enabled(user, request.user)
+
+        #  If a challenge and response exists, validate
         if challenge:
             response = request.POST.get("response")
             if response:
                 response = json.loads(response)
-                if interface.validate_response(request, challenge, response):
+                if interface.validate_response(request, challenge, response, webauthn_signin_ff):
                     return self.perform_signin(request, user, interface)
                 self.fail_signin(request, user, form)
 
@@ -158,6 +169,7 @@ class TwoFactorAuthView(BaseView):
                 "interface": interface,
                 "other_interfaces": self.get_other_interfaces(interface, interfaces),
                 "activation": activation,
+                "isWebauthnSigninFFEnabled": webauthn_signin_ff,
             },
             request,
             status=200,