From 8040e2b6e49b338277664fc680944ffe9ab5d125 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Oct 2021 23:26:29 +0200 Subject: [PATCH] build(deps): bump webauthn from 0.4.7 to 1.0.0 (#1625) * build(deps): bump webauthn from 0.4.7 to 1.0.0 Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 0.4.7 to 1.0.0. - [Release notes](https://github.com/duo-labs/py_webauthn/releases) - [Commits](https://github.com/duo-labs/py_webauthn/compare/v0.4.7...v1.0.0) --- updated-dependencies: - dependency-name: webauthn dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * stages/authenticator_webauthn: migrate to new library version Signed-off-by: Jens Langhammer * stages/authenticator_validate: migrate to new version Signed-off-by: Jens Langhammer * stages/authenticator_webauthn: add bytes_to_base64url_dict for json encoding Signed-off-by: Jens Langhammer * actually don't do that Signed-off-by: Jens Langhammer * fix missing response on web Signed-off-by: Jens Langhammer * more double json Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer * more base64 stuff Signed-off-by: Jens Langhammer * working Signed-off-by: Jens Langhammer * ci: always sync Signed-off-by: Jens Langhammer * fix Signed-off-by: Jens Langhammer Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jens Langhammer --- .github/workflows/ci-main.yml | 5 +- Pipfile.lock | 48 ++++++-- authentik/flows/views/executor.py | 26 ----- authentik/root/settings.py | 4 - .../authenticator_validate/challenge.py | 98 ++++++----------- .../stages/authenticator_validate/tests.py | 12 +- .../stages/authenticator_webauthn/models.py | 7 ++ .../stages/authenticator_webauthn/stage.py | 104 +++++++----------- .../stages/authenticator_webauthn/utils.py | 22 ---- .../stages/authenticator_webauthn/utils.ts | 33 ++++-- .../stages/UserSettingsAuthenticatorDuo.ts | 4 +- .../stages/UserSettingsAuthenticatorSMS.ts | 4 +- .../stages/UserSettingsAuthenticatorStatic.ts | 4 +- .../stages/UserSettingsAuthenticatorTOTP.ts | 4 +- .../UserSettingsAuthenticatorWebAuthn.ts | 4 +- 15 files changed, 169 insertions(+), 210 deletions(-) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 1ac8c4c01..7bca5c106 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -150,7 +150,10 @@ jobs: - name: prepare env: INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} - run: scripts/ci_prepare.sh + run: | + scripts/ci_prepare.sh + # Sync anyways since stable will have different dependencies + pipenv sync --dev - name: run migrations to stable run: pipenv run python -m lifecycle.migrate - name: checkout current code diff --git a/Pipfile.lock b/Pipfile.lock index a806d83f5..55da49f91 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -80,6 +80,13 @@ "markers": "python_version >= '3.6'", "version": "==3.4.1" }, + "asn1crypto": { + "hashes": [ + "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8", + "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c" + ], + "version": "==1.4.0" + }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -489,13 +496,6 @@ "index": "pypi", "version": "==3.1.0" }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.18.2" - }, "geoip2": { "hashes": [ "sha256:f150bed3190d543712a17467208388d31bd8ddb49b2226fba53db8aaedb8ba89", @@ -991,6 +991,34 @@ "index": "pypi", "version": "==3.11.0" }, + "pydantic": { + "hashes": [ + "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd", + "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739", + "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f", + "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840", + "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23", + "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287", + "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62", + "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b", + "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb", + "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820", + "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3", + "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b", + "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e", + "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3", + "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316", + "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b", + "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4", + "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20", + "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e", + "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505", + "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1", + "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==1.8.2" + }, "pyjwt": { "hashes": [ "sha256:a0b9a3b4e5ca5517cac9f1a6e9cd30bf1aa80be74fcdf4e28eded582ecfcfbae", @@ -1301,11 +1329,11 @@ }, "webauthn": { "hashes": [ - "sha256:238391b2e2cc60fb51a2cd2d2d6be149920b9af6184651353d9f95856617a9e7", - "sha256:8ad9072ff1d6169f3be30d4dc8733ea563dd266962397bc58b40f674a6af74ac" + "sha256:1c068b93ab0f03fc6905e42e42e6ad24caa4f2632ff13ab846d5e2ef0bf1aa37", + "sha256:6710b8b3d846010fcf303d4fb96ca42de154aeeec379ead4e18cc582a11f9abc" ], "index": "pypi", - "version": "==0.4.7" + "version": "==1.0.0" }, "websocket-client": { "hashes": [ diff --git a/authentik/flows/views/executor.py b/authentik/flows/views/executor.py index d08d31d42..a1a4691ae 100644 --- a/authentik/flows/views/executor.py +++ b/authentik/flows/views/executor.py @@ -17,13 +17,10 @@ from django.views.generic import View from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema from rest_framework.permissions import AllowAny -from rest_framework.request import Request -from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView from sentry_sdk import capture_exception from structlog.stdlib import BoundLogger, get_logger -from authentik.api.throttle import SessionThrottle from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.events.models import Event, EventAction, cleanse_dict from authentik.flows.challenge import ( @@ -100,33 +97,10 @@ class InvalidStageError(SentryIgnoredException): """Error raised when a challenge from a stage is not valid""" -class FlowPendingUserThrottle(ScopedRateThrottle): - """Custom throttle based on which user is pending""" - - def get_cache_key(self, request: Request, view) -> str: - if SESSION_KEY_PLAN not in request._request.session: - return "" - if PLAN_CONTEXT_PENDING_USER not in request._request.session[SESSION_KEY_PLAN].context: - return "" - user = request._request.session[SESSION_KEY_PLAN].context[PLAN_CONTEXT_PENDING_USER] - return f"authentik-throttle-flow-pending-{user.uid}" - - def allow_request(self, request: Request, view) -> bool: - if SESSION_KEY_PLAN not in request._request.session: - return True - if PLAN_CONTEXT_PENDING_USER not in request._request.session[SESSION_KEY_PLAN].context: - return True - if request._request.user.is_superuser: - return True - return super().allow_request(request, view) - - @method_decorator(xframe_options_sameorigin, name="dispatch") class FlowExecutorView(APIView): """Stage 1 Flow executor, passing requests to Stage Views""" - throttle_classes = [SessionThrottle, FlowPendingUserThrottle] - throttle_scope = "flow_executor" permission_classes = [AllowAny] flow: Flow diff --git a/authentik/root/settings.py b/authentik/root/settings.py index cf60e7c37..c879b3590 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -205,10 +205,6 @@ REST_FRAMEWORK = { ], "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "TEST_REQUEST_DEFAULT_FORMAT": "json", - "DEFAULT_THROTTLE_RATES": { - "anon": "100/day", - "flow_executor": "100/day", - }, } REDIS_PROTOCOL_PREFIX = "redis://" diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py index 470ad4c01..33788bf02 100644 --- a/authentik/stages/authenticator_validate/challenge.py +++ b/authentik/stages/authenticator_validate/challenge.py @@ -1,4 +1,7 @@ """Validation stage challenge checking""" +from json import dumps, loads +from typing import Optional + from django.http import HttpRequest from django.http.response import Http404 from django.shortcuts import get_object_or_404 @@ -8,12 +11,10 @@ from django_otp.models import Device from rest_framework.fields import CharField, JSONField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger -from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser -from webauthn.webauthn import ( - AuthenticationRejectedException, - RegistrationRejectedException, - WebAuthnUserDataMissing, -) +from webauthn import generate_authentication_options, verify_authentication_response +from webauthn.helpers import base64url_to_bytes, options_to_json +from webauthn.helpers.exceptions import InvalidAuthenticationResponse +from webauthn.helpers.structs import AuthenticationCredential from authentik.core.api.utils import PassiveSerializer from authentik.core.models import User @@ -21,7 +22,7 @@ from authentik.lib.utils.http import get_client_ip from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice from authentik.stages.authenticator_sms.models import SMSDevice from authentik.stages.authenticator_webauthn.models import WebAuthnDevice -from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin +from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id LOGGER = get_logger() @@ -42,40 +43,26 @@ def get_challenge_for_device(request: HttpRequest, device: Device) -> dict: return {} -def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict: +def get_webauthn_challenge(request: HttpRequest, device: Optional[WebAuthnDevice] = None) -> dict: """Send the client a challenge that we'll check later""" request.session.pop("challenge", None) - challenge = generate_challenge(32) + allowed_credentials = [] - # We strip the padding from the challenge stored in the session - # for the reasons outlined in the comment in webauthn_begin_activate. - request.session["challenge"] = challenge.rstrip("=") + if device: + # We want all the user's WebAuthn devices and merge their challenges + for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"): + user_device: WebAuthnDevice + allowed_credentials.append(user_device.descriptor) - assertion = {} - user = device.user + authentication_options = generate_authentication_options( + rp_id=get_rp_id(request), + allow_credentials=allowed_credentials, + ) - # We want all the user's WebAuthn devices and merge their challenges - for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"): - webauthn_user = WebAuthnUser( - user.uid, - user.username, - user.name, - user.avatar, - user_device.credential_id, - user_device.public_key, - user_device.sign_count, - user_device.rp_id, - ) - webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) - if assertion == {}: - assertion = webauthn_assertion_options.assertion_dict - else: - assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get( - "allowCredentials" - ) + request.session["challenge"] = authentication_options.challenge - return assertion + return loads(options_to_json(authentication_options)) def select_challenge(request: HttpRequest, device: Device): @@ -99,45 +86,32 @@ def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str: return code +# pylint: disable=unused-argument def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) -> dict: """Validate WebAuthn Challenge""" challenge = request.session.get("challenge") - assertion_response = data - credential_id = assertion_response.get("id") + credential_id = data.get("id") device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() if not device: raise ValidationError("Device does not exist.") - webauthn_user = WebAuthnUser( - user.uid, - user.username, - user.name, - user.avatar, - device.credential_id, - device.public_key, - device.sign_count, - device.rp_id, - ) - - webauthn_assertion_response = WebAuthnAssertionResponse( - webauthn_user, - assertion_response, - challenge, - get_origin(request), - uv_required=False, - ) # User Verification - try: - sign_count = webauthn_assertion_response.verify() - except ( - AuthenticationRejectedException, - WebAuthnUserDataMissing, - RegistrationRejectedException, - ) as exc: + authentication_verification = verify_authentication_response( + credential=AuthenticationCredential.parse_raw(dumps(data)), + expected_challenge=challenge, + expected_rp_id=get_rp_id(request), + expected_origin=get_origin(request), + credential_public_key=base64url_to_bytes(device.public_key), + credential_current_sign_count=device.sign_count, + require_user_verification=False, + ) + + except (InvalidAuthenticationResponse) as exc: + LOGGER.warning("Assertion failed", exc=exc) raise ValidationError("Assertion failed") from exc - device.set_sign_count(sign_count) + device.set_sign_count(authentication_verification.new_sign_count) return data diff --git a/authentik/stages/authenticator_validate/tests.py b/authentik/stages/authenticator_validate/tests.py index 27b2cca24..3f6f24c76 100644 --- a/authentik/stages/authenticator_validate/tests.py +++ b/authentik/stages/authenticator_validate/tests.py @@ -7,6 +7,7 @@ from django.utils.encoding import force_str from django_otp.plugins.otp_totp.models import TOTPDevice from rest_framework.exceptions import ValidationError from rest_framework.test import APITestCase +from webauthn.helpers import bytes_to_base64url from authentik.core.models import User from authentik.flows.challenge import ChallengeTypes @@ -101,8 +102,8 @@ class AuthenticatorValidateStageTests(APITestCase): webauthn_device = WebAuthnDevice.objects.create( user=self.user, - public_key="qwerqwerqre", - credential_id="foobarbaz", + public_key=bytes_to_base64url(b"qwerqwerqre"), + credential_id=bytes_to_base64url(b"foobarbaz"), sign_count=0, rp_id="foo", ) @@ -113,14 +114,13 @@ class AuthenticatorValidateStageTests(APITestCase): { "allowCredentials": [ { - "id": "foobarbaz", - "transports": ["usb", "nfc", "ble", "internal"], + "id": "Zm9vYmFyYmF6", "type": "public-key", } ], - "rpId": "foo", + "rpId": "testserver", "timeout": 60000, - "userVerification": "discouraged", + "userVerification": "preferred", }, ) diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py index 2131fc5f2..a28b52c8b 100644 --- a/authentik/stages/authenticator_webauthn/models.py +++ b/authentik/stages/authenticator_webauthn/models.py @@ -8,6 +8,8 @@ from django.utils.translation import gettext_lazy as _ from django.views import View from django_otp.models import Device from rest_framework.serializers import BaseSerializer +from webauthn.helpers.base64url_to_bytes import base64url_to_bytes +from webauthn.helpers.structs import PublicKeyCredentialDescriptor from authentik.core.types import UserSettingSerializer from authentik.flows.models import ConfigurableStage, Stage @@ -64,6 +66,11 @@ class WebAuthnDevice(Device): created_on = models.DateTimeField(auto_now_add=True) last_used_on = models.DateTimeField(default=now) + @property + def descriptor(self) -> PublicKeyCredentialDescriptor: + """Get a publickeydescriptor for this device""" + return PublicKeyCredentialDescriptor(id=base64url_to_bytes(self.credential_id)) + def set_sign_count(self, sign_count: int) -> None: """Set the sign_count and update the last_used_on datetime.""" self.sign_count = sign_count diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index 966a43735..f2642820b 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -1,16 +1,22 @@ """WebAuthn stage""" +from json import dumps, loads from django.http import HttpRequest, HttpResponse from django.http.request import QueryDict from rest_framework.fields import CharField, JSONField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger -from webauthn.webauthn import ( - RegistrationRejectedException, - WebAuthnCredential, - WebAuthnMakeCredentialOptions, - WebAuthnRegistrationResponse, +from webauthn import generate_registration_options, options_to_json, verify_registration_response +from webauthn.helpers import bytes_to_base64url +from webauthn.helpers.exceptions import InvalidRegistrationResponse +from webauthn.helpers.structs import ( + AuthenticatorSelectionCriteria, + PublicKeyCredentialCreationOptions, + RegistrationCredential, + ResidentKeyRequirement, + UserVerificationRequirement, ) +from webauthn.registration.verify_registration_response import VerifiedRegistration from authentik.core.models import User from authentik.flows.challenge import ( @@ -22,7 +28,7 @@ from authentik.flows.challenge import ( from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ChallengeStageView from authentik.stages.authenticator_webauthn.models import WebAuthnDevice -from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin, get_rp_id +from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id LOGGER = get_logger() @@ -47,46 +53,29 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): def validate_response(self, response: dict) -> dict: """Validate webauthn challenge response""" + # pylint: disable=no-name-in-module + from pydantic.error_wrappers import ValidationError as PydanticValidationError + challenge = self.request.session["challenge"] - trusted_attestation_cert_required = True - self_attestation_permitted = True - none_attestation_permitted = True - - webauthn_registration_response = WebAuthnRegistrationResponse( - get_rp_id(self.request), - get_origin(self.request), - response, - challenge, - trusted_attestation_cert_required=trusted_attestation_cert_required, - self_attestation_permitted=self_attestation_permitted, - none_attestation_permitted=none_attestation_permitted, - uv_required=False, - ) # User Verification - try: - webauthn_credential = webauthn_registration_response.verify() - except RegistrationRejectedException as exc: + registration: VerifiedRegistration = verify_registration_response( + credential=RegistrationCredential.parse_raw(dumps(response)), + expected_challenge=challenge, + expected_rp_id=get_rp_id(self.request), + expected_origin=get_origin(self.request), + ) + except (InvalidRegistrationResponse, PydanticValidationError) as exc: LOGGER.warning("registration failed", exc=exc) raise ValidationError(f"Registration failed. Error: {exc}") - # Step 17. - # - # Check that the credentialId is not yet registered to any other user. - # If registration is requested for a credential that is already registered - # to a different user, the Relying Party SHOULD fail this registration - # ceremony, or it MAY decide to accept the registration, e.g. while deleting - # the older registration. credential_id_exists = WebAuthnDevice.objects.filter( - credential_id=webauthn_credential.credential_id + credential_id=bytes_to_base64url(registration.credential_id) ).first() if credential_id_exists: raise ValidationError("Credential ID already exists.") - webauthn_credential.credential_id = str(webauthn_credential.credential_id, "utf-8") - webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8") - - return webauthn_credential + return registration class AuthenticatorWebAuthnStageView(ChallengeStageView): @@ -98,35 +87,26 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): # clear session variables prior to starting a new registration self.request.session.pop("challenge", None) - challenge = generate_challenge(32) - - # We strip the saved challenge of padding, so that we can do a byte - # comparison on the URL-safe-without-padding challenge we get back - # from the browser. - # We will still pass the padded version down to the browser so that the JS - # can decode the challenge into binary without too much trouble. - self.request.session["challenge"] = challenge.rstrip("=") user = self.get_pending_user() - make_credential_options = WebAuthnMakeCredentialOptions( - challenge, - self.request.tenant.branding_title, - get_rp_id(self.request), - user.uid, - user.username, - user.name, - user.avatar, + + registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( + rp_id=get_rp_id(self.request), + rp_name=self.request.tenant.branding_title, + user_id=user.uid, + user_name=user.username, + user_display_name=user.name, + authenticator_selection=AuthenticatorSelectionCriteria( + resident_key=ResidentKeyRequirement.PREFERRED, + user_verification=UserVerificationRequirement.PREFERRED, + ), ) + registration_options.user.id = user.uid - registration_dict = make_credential_options.registration_dict - registration_dict["authenticatorSelection"] = { - "requireResidentKey": False, - "userVerification": "preferred", - } - + self.request.session["challenge"] = registration_options.challenge return AuthenticatorWebAuthnChallenge( data={ "type": ChallengeTypes.NATIVE.value, - "registration": registration_dict, + "registration": loads(options_to_json(registration_options)), } ) @@ -145,15 +125,15 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # Webauthn Challenge has already been validated - webauthn_credential: WebAuthnCredential = response.validated_data["response"] + webauthn_credential: VerifiedRegistration = response.validated_data["response"] existing_device = WebAuthnDevice.objects.filter( - credential_id=webauthn_credential.credential_id + credential_id=bytes_to_base64url(webauthn_credential.credential_id) ).first() if not existing_device: WebAuthnDevice.objects.create( user=self.get_pending_user(), - public_key=webauthn_credential.public_key, - credential_id=webauthn_credential.credential_id, + public_key=bytes_to_base64url(webauthn_credential.credential_public_key), + credential_id=bytes_to_base64url(webauthn_credential.credential_id), sign_count=webauthn_credential.sign_count, rp_id=get_rp_id(self.request), ) diff --git a/authentik/stages/authenticator_webauthn/utils.py b/authentik/stages/authenticator_webauthn/utils.py index 881066ef8..3c06c5fd9 100644 --- a/authentik/stages/authenticator_webauthn/utils.py +++ b/authentik/stages/authenticator_webauthn/utils.py @@ -1,29 +1,7 @@ """webauthn utils""" -import base64 -import os from django.http import HttpRequest -CHALLENGE_DEFAULT_BYTE_LEN = 32 - - -def generate_challenge(challenge_len=CHALLENGE_DEFAULT_BYTE_LEN): - """Generate a challenge of challenge_len bytes, Base64-encoded. - We use URL-safe base64, but we *don't* strip the padding, so that - the browser can decode it without too much hassle. - Note that if we are doing byte comparisons with the challenge in collectedClientData - later on, that value will not have padding, so we must remove the padding - before storing the value in the session. - """ - # If we know Python 3.6 or greater is available, we could replace this with one - # call to secrets.token_urlsafe - challenge_bytes = os.urandom(challenge_len) - challenge_base64 = base64.urlsafe_b64encode(challenge_bytes) - # Python 2/3 compatibility: b64encode returns bytes only in newer Python versions - if not isinstance(challenge_base64, str): - challenge_base64 = challenge_base64.decode("utf-8") - return challenge_base64 - def get_rp_id(request: HttpRequest) -> str: """Get hostname from http request, without port""" diff --git a/web/src/flows/stages/authenticator_webauthn/utils.ts b/web/src/flows/stages/authenticator_webauthn/utils.ts index bae6640a4..77be2556e 100644 --- a/web/src/flows/stages/authenticator_webauthn/utils.ts +++ b/web/src/flows/stages/authenticator_webauthn/utils.ts @@ -1,7 +1,5 @@ import * as base64js from "base64-js"; -import { hexEncode } from "../../../utils"; - export function b64enc(buf: Uint8Array): string { return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } @@ -33,9 +31,11 @@ export interface Assertion { id: string; rawId: string; type: string; - attObj: string; - clientData: string; registrationClientExtensions: string; + response: { + clientDataJSON: string; + attestationObject: string; + }; } /** @@ -55,9 +55,11 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential id: newAssertion.id, rawId: b64enc(rawId), type: newAssertion.type, - attObj: b64enc(attObj), - clientData: b64enc(clientDataJSON), registrationClientExtensions: JSON.stringify(registrationClientExtensions), + response: { + clientDataJSON: b64enc(clientDataJSON), + attestationObject: b64enc(attObj), + }, }; } @@ -91,10 +93,13 @@ export interface AuthAssertion { id: string; rawId: string; type: string; - clientData: string; - authData: string; - signature: string; assertionClientExtensions: string; + response: { + clientDataJSON: string; + authenticatorData: string; + signature: string; + userHandle: string | null; + }; } /** @@ -113,9 +118,13 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential): id: newAssertion.id, rawId: b64enc(rawId), type: newAssertion.type, - authData: b64RawEnc(authData), - clientData: b64RawEnc(clientDataJSON), - signature: hexEncode(sig), assertionClientExtensions: JSON.stringify(assertionClientExtensions), + + response: { + clientDataJSON: b64RawEnc(clientDataJSON), + signature: b64RawEnc(sig), + authenticatorData: b64RawEnc(authData), + userHandle: null, + }, }; } diff --git a/web/src/user/user-settings/stages/UserSettingsAuthenticatorDuo.ts b/web/src/user/user-settings/stages/UserSettingsAuthenticatorDuo.ts index ea0743e4f..55edcc471 100644 --- a/web/src/user/user-settings/stages/UserSettingsAuthenticatorDuo.ts +++ b/web/src/user/user-settings/stages/UserSettingsAuthenticatorDuo.ts @@ -60,7 +60,9 @@ export class UserSettingsAuthenticatorDuo extends BaseUserSettings {