def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) -> Device: """Validate WebAuthn Challenge""" challenge = request.session.get("challenge") credential_id = data.get("id") device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() if not device: raise ValidationError("Device does not exist.") try: 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(authentication_verification.new_sign_count) return device
def get_challenge(self, *args, **kwargs) -> Challenge: # 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, RP_NAME, get_rp_id(self.request), user.uid, user.username, user.name, user.avatar, ) registration_dict = make_credential_options.registration_dict registration_dict["authenticatorSelection"] = { "requireResidentKey": False, "userVerification": "preferred", } return AuthenticatorWebAuthnChallenge( data={ "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-authenticator-webauthn", "registration": registration_dict, })
def get_challenge(self, *args, **kwargs) -> Challenge: # clear session variables prior to starting a new registration self.request.session.pop("challenge", None) stage: AuthenticateWebAuthnStage = self.executor.current_stage user = self.get_pending_user() # library accepts none so we store null in the database, but if there is a value # set, cast it to string to ensure it's not a django class authenticator_attachment = stage.authenticator_attachment if authenticator_attachment: authenticator_attachment = str(authenticator_attachment) 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=str(stage.resident_key_requirement), user_verification=str(stage.user_verification), authenticator_attachment=authenticator_attachment, ), ) self.request.session["challenge"] = registration_options.challenge return AuthenticatorWebAuthnChallenge( data={ "type": ChallengeTypes.NATIVE.value, "registration": loads(options_to_json(registration_options)), })
def get_webauthn_challenge_userless(request: HttpRequest) -> dict: """Same as `get_webauthn_challenge`, but allows any client device. We can then later check who the device belongs to.""" request.session.pop("challenge", None) authentication_options = generate_authentication_options( rp_id=get_rp_id(request), allow_credentials=[], ) request.session["challenge"] = authentication_options.challenge return loads(options_to_json(authentication_options))
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # Webauthn Challenge has already been validated webauthn_credential: WebAuthnCredential = response.validated_data[ "response"] existing_device = WebAuthnDevice.objects.filter( credential_id=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, sign_count=webauthn_credential.sign_count, rp_id=get_rp_id(self.request), ) else: return self.executor.stage_invalid( "Device with Credential ID already exists.") return self.executor.stage_ok()
def validate_response(self, response: dict) -> dict: """Validate webauthn challenge response""" 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: LOGGER.warning("registration failed", exc=exc) raise ValidationError("Registration failed. Error: {}".format(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).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
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) allowed_credentials = [] 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) authentication_options = generate_authentication_options( rp_id=get_rp_id(request), allow_credentials=allowed_credentials, ) request.session["challenge"] = authentication_options.challenge return loads(options_to_json(authentication_options))
def validate_response(self, response: dict) -> dict: """Validate webauthn challenge response""" challenge = self.request.session["challenge"] try: 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 as exc: LOGGER.warning("registration failed", exc=exc) raise ValidationError(f"Registration failed. Error: {exc}") credential_id_exists = WebAuthnDevice.objects.filter( credential_id=bytes_to_base64url( registration.credential_id)).first() if credential_id_exists: raise ValidationError("Credential ID already exists.") return registration
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # Webauthn Challenge has already been validated webauthn_credential: VerifiedRegistration = response.validated_data[ "response"] existing_device = WebAuthnDevice.objects.filter( credential_id=bytes_to_base64url( webauthn_credential.credential_id)).first() if not existing_device: WebAuthnDevice.objects.create( user=self.get_pending_user(), 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), name="WebAuthn Device", ) else: return self.executor.stage_invalid( "Device with Credential ID already exists.") return self.executor.stage_ok()