diff --git a/.gitmodules b/.gitmodules
index 638f4e89531a3909b695c61ba320129537b5d5b3..772abae58989a5f139eb8e3e46f891cdb2b981c6 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
 [submodule "privacyidea/static/contrib/js/webauthn-client"]
 	path = privacyidea/static/contrib/js/webauthn-client
-	url = https://github.com/privacyidea/webauthn-client
+	url = https://github.com/privacyidea/webauthn-client.git
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
index 8bf60614724b08405fcfc957cdcd84e4201d1a35..40f71062ae2cb92f20ce572a3484428a933dffbe 100644
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -2026,67 +2026,6 @@ def webauthntoken_allowed(request, action):
     :return:
     :rtype:
     """
-
-    ttype = request.all_data.get("type")
-
-    # Get the registration data of the 2nd step of enrolling a WebAuthn token
-    reg_data = request.all_data.get("regdata")
-
-    # If a WebAuthn token is being enrolled.
-    if ttype and ttype.lower() == WebAuthnTokenClass.get_class_type() and reg_data:
-        serial = request.all_data.get("serial")
-        att_obj = WebAuthnRegistrationResponse.parse_attestation_object(reg_data)
-        (
-            attestation_type,
-            trust_path,
-            credential_pub_key,
-            cred_id,
-            aaguid
-        ) = WebAuthnRegistrationResponse.verify_attestation_statement(fmt=att_obj.get('fmt'),
-                                                                      att_stmt=att_obj.get('attStmt'),
-                                                                      auth_data=att_obj.get('authData'))
-
-        attestation_cert = crypto.X509.from_cryptography(trust_path[0]) if trust_path else None
-        allowed_certs_pols = Match\
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.REQ,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False)
-
-        allowed_aaguids_pols = Match \
-            .user(g,
-                  scope=SCOPE.ENROLL,
-                  action=WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST,
-                  user_object=request.User if hasattr(request, 'User') else None) \
-            .action_values(unique=False,
-                           allow_white_space_in_action=True)
-        allowed_aaguids = set(
-            aaguid
-            for allowed_aaguid_pol in allowed_aaguids_pols
-            for aaguid in allowed_aaguid_pol.split()
-        )
-
-        # attestation_cert is of type X509. If you get a warning from your IDE
-        # here, it is because your IDE mistakenly assumes it to be of type PKey,
-        # due to a bug in pyOpenSSL 18.0.0. This bug is – however – purely
-        # cosmetic (a wrongly hinted return type in X509.from_cryptography()),
-        # and can be safely ignored.
-        #
-        # See also:
-        # https://github.com/pyca/pyopenssl/commit/4121e2555d07bbba501ac237408a0eea1b41f467
-        if allowed_certs_pols and not _attestation_certificate_allowed(attestation_cert, allowed_certs_pols):
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.REQ))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
-        if allowed_aaguids and aaguid not in [allowed_aaguid.replace("-", "") for allowed_aaguid in allowed_aaguids]:
-            log.warning(
-                "The WebAuthn token {0!s} is not allowed to be registered due to policy restriction {1!s}"
-                    .format(serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-            raise PolicyError("The WebAuthn token is not allowed to be registered due to a policy restriction.")
-
     return True
 
 
diff --git a/privacyidea/config.py b/privacyidea/config.py
index 15015c2622a43080baac177f681c3e0b3afbb903..9e388b86849daa06ffc783dd256f4b8d8741c9e2 100644
--- a/privacyidea/config.py
+++ b/privacyidea/config.py
@@ -1,14 +1,15 @@
 # -*- coding: utf-8 -*-
 
-import os
 import logging
+import os
 import random
 import string
+from sys import platform
+
 log = logging.getLogger(__name__)
 basedir = os.path.abspath(os.path.dirname(__file__))
 basedir = "/".join(basedir.split("/")[:-1]) + "/"
 
-
 pubtest_key = b"""-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0stF+PVh/qR7I82ywgDW
 X2rPxKnlBugU33SAbxZjfNz+aKYYM+irKJ0IdTOLtQ85DzZaEhocqO7cg1Qw85Ac
@@ -34,7 +35,8 @@ WQIDAQAB
 def _random_password(size):
     log.info("SECRET_KEY not set in config. Generating a random key.")
     passwd = [random.choice(string.ascii_lowercase + \
-                            string.ascii_uppercase + string.digits) for _x in range(size)]
+                            string.ascii_uppercase + string.digits) for _x in
+              range(size)]
     # return shuffled password
     random.shuffle(passwd)
     return "".join(passwd)
@@ -44,7 +46,8 @@ class Config(object):
     SECRET_KEY = os.environ.get('SECRET_KEY')
     # SQL_ALCHEMY_DATABASE_URI = "mysql://privacyidea:XmbSrlqy5d4IS08zjz"
     # "GG5HTt40Cpf5@localhost/privacyidea"
-    PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    # PI_ENCFILE = os.path.join(basedir, "tests/testdata/enckey")
+    PI_ENCFILE = os.path.join("tests/testdata/enckey")
     PI_HSM = "default"
     PI_AUDIT_MODULE = "privacyidea.lib.auditmodules.sqlaudit"
     PI_AUDIT_KEY_PRIVATE = os.path.join(basedir, "tests/testdata/private.pem")
@@ -65,7 +68,8 @@ class DevelopmentConfig(Config):
     DEBUG = True
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data-dev.sqlite')
     PI_LOGLEVEL = logging.DEBUG
     PI_TRANSLATION_WARNING = "[Missing]"
 
@@ -75,8 +79,14 @@ class TestingConfig(Config):
     # This is used to encrypt the auth token
     SUPERUSER_REALM = ['adminrealm']
     SECRET_KEY = 'secret'
-    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
+    if platform == 'win32':
+        SQLITE_DB_PATH = 'sqlite:///' + os.path.join(basedir, 'Users',
+                                                     os.getlogin(),
+                                                     'Documents',
+                                                     'data-test.sqlite')
+    SQLALCHEMY_DATABASE_URI = os.environ.get(
+        'TEST_DATABASE_URL') or SQLITE_DB_PATH
     # This is used to encrypt the admin passwords
     PI_PEPPER = ""
     # This is only for testing encrypted files
@@ -124,8 +134,9 @@ class AltUIConfig(TestingConfig):
 
 class ProductionConfig(Config):
     SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
-        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+                              'sqlite:///' + os.path.join(basedir,
+                                                          'data.sqlite')
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or _random_password(24)
     # This is used to encrypt the admin passwords
@@ -144,7 +155,7 @@ class HerokuConfig(Config):
                               "wqy_btZE3CPPNWsmkfdmeorxy6@" \
                               "ec2-54-83-0-61.compute-1." \
                               "amazonaws.com:5432/d6fjidokoeilp6"
-    #SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
+    # SQLALCHEMY_DATABASE_URI = "mysql://pi2:pi2@localhost/pi2"
     # This is used to encrypt the auth_token
     SECRET_KEY = os.environ.get('SECRET_KEY') or 't0p s3cr3t'
     # This is used to encrypt the admin passwords
diff --git a/privacyidea/lib/config.py b/privacyidea/lib/config.py
index 819100544e4ba615c8011c99eaf85228a7e06bf6..10a948078a16e42f99cacbf69518da934a4b4d10 100644
--- a/privacyidea/lib/config.py
+++ b/privacyidea/lib/config.py
@@ -699,6 +699,7 @@ def get_token_list():
     """
     module_list = set()
 
+    module_list.add("privacyidea.lib.tokens.backupcodetoken")
     module_list.add("privacyidea.lib.tokens.daplugtoken")
     module_list.add("privacyidea.lib.tokens.hotptoken")
     module_list.add("privacyidea.lib.tokens.motptoken")
diff --git a/privacyidea/lib/tokens/backupcodetoken.py b/privacyidea/lib/tokens/backupcodetoken.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d64dfbf7895e995776c0b65a532ea5fbb90bdec
--- /dev/null
+++ b/privacyidea/lib/tokens/backupcodetoken.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+#
+#  2022-09-22 Peter Bolha <485456@mail.muni.cz>
+#             BUC token with to be randomly used BUC values
+#
+#  (c) 2022 Peter Bolha - 485456@mail.muni.cz
+#
+# This code is free software; you can redistribute it and/or
+# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or any later version.
+#
+# This code is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+This file contains the definition of the backupcode token class
+It depends on the DB model, and the lib.tokenclass.
+"""
+
+import ast
+import logging
+import secrets
+import string
+
+from privacyidea.lib import _
+from privacyidea.lib.decorators import check_token_locked
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import ACTION, GROUP, SCOPE
+from privacyidea.lib.tokenclass import TokenClass
+
+log = logging.getLogger(__name__)
+
+# number of generated tokens per method call
+DEFAULT_COUNT = 10
+# length of each token
+DEFAULT_LENGTH = 16
+CHARACTER_POOL = list(string.digits) + list(string.ascii_letters)
+SECRET_GENERATOR = secrets.SystemRandom()
+
+
+class BACKUPCODEACTION(object):
+    BACKUPCODETOKEN_COUNT = "backupcodetoken_count"
+    BACKUPCODETOKEN_LENGTH = "backupcodetoken_length"
+
+
+class BackupCodeTokenClass(TokenClass):
+    """
+    The BUC token allows printing out valid OTP values.
+    This sheet of paper can be used to authenticate and strike out the used
+    OTP values. It works akin to TAN token (OTPs can be used in random order).
+    The difference is that the number and length of the generated OTPs can be
+    configured as well as the character pool for code generation. The default
+    settings: 10 codes, 16 characters long, generated from alphanumeric
+    charset. (a-zA-Z0-9)
+    """
+
+    @log_with(log)
+    def __init__(self, db_token):
+        """
+        This creates a new Backup Code Token object from a DB token object.
+
+        :param db_token: instance of the orm db object
+        :type db_token:  orm object
+        """
+        TokenClass.__init__(self, db_token)
+        self.set_type(u"backupcode")
+        self.hKeyRequired = False
+
+    @staticmethod
+    def get_class_type():
+        """
+        Return the token type shortname.
+
+        :return: 'backupcode'
+        :rtype: string
+        """
+        return "backupcode"
+
+    @staticmethod
+    def get_class_prefix():
+        """
+        Return the prefix, that is used as a prefix for the serial.
+        numbers
+        :return: BUC
+        """
+        return "BUC"
+
+    @staticmethod
+    @log_with(log)
+    def get_class_info(key=None, ret='all'):
+        """
+        Returns a subtree of the token definition
+
+        :param key: subsection identifier
+        :type key: string
+        :param ret: default return value, if nothing is found
+        :type ret: user defined
+        :return: subsection if key exists or user defined
+        :rtype: dict or scalar
+        """
+        res = {'type': 'backupcode',
+               'title': 'Backup Code Token',
+               'description': 'BUC: Backup codes generated by user.',
+               'init': {},
+               'config': {},
+               'user': ['enroll'],
+               'ui_enroll': ["admin", "user"],
+               'policy': {
+                   SCOPE.ENROLL: {
+                       BACKUPCODEACTION.BACKUPCODETOKEN_COUNT: {
+                           "type": "int",
+                           "desc": _("The number of OTP values, which are "
+                                     "generated by user.")
+                       },
+                       BACKUPCODEACTION.BACKUPCODETOKEN_LENGTH: {
+                           "type": "int",
+                           "desc": _("Length of each generated token.")
+                       },
+                       ACTION.MAXTOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       },
+                       ACTION.MAXACTIVETOKENUSER: {
+                           'type': 'int',
+                           'desc': _(
+                               "The user may only have this maximum number "
+                               "of active backup code tokens assigned."),
+                           'group': GROUP.TOKEN
+                       }
+
+                   }
+               }
+               }
+
+        if key:
+            ret = res.get(key, {})
+        else:
+            if ret == 'all':
+                ret = res
+        return ret
+
+    def generate_codes(self, length, count):
+        """
+        Generates a list with <count> number of alphanumeric codes with
+        length of <length> characters picked from CHARACTER_POOL.
+
+        :param length: length of each generated token in characters
+        :type length: int
+        :param count: total number of tokens to generate
+        :type count: int
+        :return: list of tokens generated with preconfigured length, number and
+                 character set
+        :rtype: list
+        """
+        return ["".join(SECRET_GENERATOR.choices(CHARACTER_POOL, k=length)) for
+                _ in range(count)]
+
+    def check_otp(self, otpval, counter=None, window=None, options=None):
+        """
+        Check if the given OTP value is valid for this token.
+
+        :param anOtpVal: the to be verified otpvalue
+        :type anOtpVal: string
+        :param counter: the counter state, that should be verified
+        :type counter: int
+        :param window: the counter +window, which should be checked
+        :type window: int
+        :param options: the dict, which could contain token specific
+        info
+        :type options: dict
+        :return: the counter state or -1
+        :rtype: int
+        """
+        token_info = self.get_tokeninfo()
+        all_user_codes = ast.literal_eval(token_info.get("otps", []))
+        used_codes = ast.literal_eval(token_info.get("used_otps", []))
+
+        if otpval in used_codes:
+            return -1
+
+        if otpval in all_user_codes:
+            used_codes.append(otpval)
+            token_info["used_otps"] = used_codes
+            self.set_tokeninfo(token_info)
+            return 1
+
+        return -1
+
+    @check_token_locked
+    def authenticate(self, passw, user=None, options=None):
+        """
+        High level interface which covers the check_pin and check_otp
+        This is the method that verifies single shot authentication
+        like they are done with push button tokens.
+
+        It is automatically called for OTPs longer than 6 characters.
+        Passw can be either a plain OTP token or a pin concatenated with an
+        OTP.
+        Pin can be either prepended in front of the OTP (default) - like
+        pinOTP or
+        it can be appended after the OTP - like OTPpin. Appending or prepending
+        a pin can be configured in the TokenClass config.
+
+        If the authentication succeeds an OTP counter needs to be
+        increased,
+        i.e. the OTP value that was used for this authentication is
+        invalidated!
+
+        :param passw: the password which could be pin+otp value
+        :type passw: string
+        :param user: The authenticating user
+        :type user: User object
+        :param options: dictionary of additional request parameters
+        :type options: dict
+
+        :return: returns tuple of
+
+            1. true or false for the pin match,
+            2. the otpcounter (int) and the
+            3. reply (dict) that will be added as additional
+            information in
+                the JSON response of ``/validate/check``.
+
+        :rtype: tuple(bool, int, dict)
+        """
+        is_pin_correct = False
+        otp_counter = -1
+        reply = None
+
+        (is_split_successfully, pin, otpval) = self.split_pin_pass(passw,
+                                                                   user=user,
+                                                                   options=options)
+        if is_split_successfully:
+            is_pin_correct = self.check_pin(pin, user=user, options=options)
+            if is_pin_correct:
+                otp_counter = self.check_otp(otpval, options=options)
+
+        return is_pin_correct, otp_counter, reply
+
+    def update(self, param, reset_failcount=True):
+        """
+        Update the token object
+
+        :param param: a dictionary with different params
+        like keysize,description, genkey, otpkey, pin
+        :type: param: dict
+        """
+        token_count = int(param.get("backupcodetoken_count", DEFAULT_COUNT))
+        token_length = int(param.get("backupcodetoken_length", DEFAULT_LENGTH))
+        param["otplen"] = token_length
+        TokenClass.update(self, param, reset_failcount=reset_failcount)
+        backup_codes = self.generate_codes(length=token_length,
+                                           count=token_count)
+        self.add_init_details("otps", backup_codes)
+        self.add_tokeninfo("otps", backup_codes)
+        self.add_tokeninfo("used_otps", [])
diff --git a/privacyidea/lib/tokens/webauthn.py b/privacyidea/lib/tokens/webauthn.py
index 72423b1141f55ca96b7ea57708b57963a5a91ffe..0f79c87df0bf906c9c1265878b5720f93029a176 100644
--- a/privacyidea/lib/tokens/webauthn.py
+++ b/privacyidea/lib/tokens/webauthn.py
@@ -66,11 +66,19 @@ from cryptography import x509
 from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import constant_time
-from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers, SECP256R1, ECDSA
-from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, MGF1
+from cryptography.hazmat.primitives.asymmetric.ec import \
+    EllipticCurvePublicNumbers, SECP256R1, ECDSA
+from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, PSS, \
+    MGF1
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
-from cryptography.hazmat.primitives.hashes import SHA256, SHA1
+from cryptography.hazmat.primitives.hashes import SHA256
 from cryptography.x509 import load_der_x509_certificate
+from webauthn import verify_registration_response, base64url_to_bytes
+from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidRegistrationResponse
+from webauthn.helpers.structs import RegistrationCredential
+from webauthn.registration.verify_registration_response import \
+    VerifiedRegistration
 
 from privacyidea.lib.tokens.u2f import url_encode, url_decode
 from privacyidea.lib.utils import to_bytes, to_unicode
@@ -116,6 +124,7 @@ class ATTESTATION_TYPE(object):
     NONE = 'None'
     ATTESTATION_CA = 'AttCA'
     SELF_ATTESTATION = 'Self'
+    ANONYMIZATION_CA = 'AnonCA'
 
 
 # Only supporting 'None', 'Basic', and 'Self Attestation' attestation types for now.
@@ -183,7 +192,6 @@ class COSE_ALGORITHM(object):
     ES256 = -7
     PS256 = -37
     RS256 = -257
-    RS1 = -65535  # for tests, otherwise unsupported
 
 
 SUPPORTED_COSE_ALGORITHMS = (
@@ -426,9 +434,9 @@ class WebAuthnMakeCredentialOptions(object):
         :type rp_id: basestring
         :param user_id: The ID for the user credential being generated. This is the privacyIDEA token serial.
         :type user_id: basestring
-        :param user_name: The username the user logs in with.
+        :param user_name: The user name the user logs in with.
         :type user_name: basestring
-        :param user_display_name: The human-readable name of the user.
+        :param user_display_name: The human readable name of the user.
         :type user_display_name: basestring
         :param icon_url: An optional icon url.
         :type icon_url: basestring
@@ -665,9 +673,9 @@ class WebAuthnUser(object):
 
         :param user_id: The ID for the user credential being stored. This is the privacyIDEA token serial.
         :type user_id: basestring
-        :param user_name: The username the user logs in with.
+        :param user_name: The user name the user logs in with.
         :type user_name: basestring
-        :param user_display_name: The human-readable name of the user.
+        :param user_display_name: The human readable name of the user.
         :type user_display_name: basestring
         :param icon_url: An optional icon url.
         :type icon_url: basestring
@@ -700,8 +708,7 @@ class WebAuthnUser(object):
         self.rp_id = rp_id
 
     def __str__(self):
-        return '{!r} ({}, {}, {})'.format(self.user_id, self.user_name,
-                                          self.user_display_name, self.sign_count)
+        return '{} ({}, {}, {})'.format(self.user_id, self.user_name, self.user_display_name, self.sign_count)
 
 
 class WebAuthnCredential(object):
@@ -772,9 +779,8 @@ class WebAuthnCredential(object):
 
         return not ATTESTATION_REQUIREMENT_LEVEL[self.attestation_level]['self_attestation_permitted']
 
-    def __str__(self):
-        return '{!r} ({}, {}, {})'.format(self.credential_id, self.rp_id,
-                                          self.origin, self.sign_count)
+    def __str_(self):
+        return '{} ({}, {}, {})'.format(self.credential_id, self.rp_id, self.origin, self.sign_count)
 
 
 class WebAuthnRegistrationResponse(object):
@@ -1168,56 +1174,13 @@ class WebAuthnRegistrationResponse(object):
                 #   * If successful, return attestation type ECDAA and
                 #     attestation trust path ecdaaKeyId.
                 raise RegistrationRejectedException('ECDAA attestation type is not currently supported.')
-            else:
-                # self-attestation
-                # Step 1:
-                # Validate that alg matches the algorithm of the credentialPublicKey
-                # in authenticatorData.
-                try:
-                    public_key_alg, credential_public_key = _load_cose_public_key(credential_pub_key)
-                except COSEKeyException as e:
-                    raise RegistrationRejectedException(str(e))
-                if alg != public_key_alg:
-                    raise RegistrationRejectedException('credentialPublicKey algorithm {0!s} does '
-                                                        'not match algorithm from attestation '
-                                                        'statement {1!s}'.format(public_key_alg, alg))
-                # Step 2:
-                # Verify that sig is a valid signature over the concatenation of authenticatorData
-                # and clientDataHash using the credential public key with alg.
-                try:
-                    _verify_signature(credential_public_key, alg, verification_data, signature)
-                except InvalidSignature:
-                    raise RegistrationRejectedException('Invalid signature received.')
-                except NotImplementedError:  # pragma: no cover
-                    log.warning('Unsupported algorithm ({0!s}) for signature '
-                                'verification'.format(alg))
-                    # We do not support this algorithm. Treat as none attestation, if acceptable.
-                    if none_attestation_permitted:
-                        return (
-                            ATTESTATION_TYPE.NONE,
-                            [],
-                            credential_pub_key,
-                            cred_id,
-                            aaguid
-                        )
-                    else:
-                        raise RegistrationRejectedException('Unsupported algorithm '
-                                                            '({0!s}).'.format(alg))
-                return (
-                    ATTESTATION_TYPE.SELF_ATTESTATION,
-                    [],
-                    credential_pub_key,
-                    cred_id,
-                    aaguid
-                )
         else:
             # Attestation is either none, or unsupported.
             if not none_attestation_permitted:
                 if fmt == ATTESTATION_FORMAT.NONE:
                     raise RegistrationRejectedException('Authenticator attestation is required.')
                 else:
-                    raise RegistrationRejectedException(
-                        'Unsupported authenticator attestation format ({0!s})!'.format(fmt))
+                    raise RegistrationRejectedException('Unsupported authenticator attestation format.')
 
             # Treat as none attestation.
             #
@@ -1234,256 +1197,64 @@ class WebAuthnRegistrationResponse(object):
                 aaguid
             )
 
+    def extract_credential_id(self):
+        attestation_object = self.registration_response["attObj"]
+        decoded_attestation_object = cbor2.loads(webauthn_b64_decode(attestation_object))
+        auth_data = decoded_attestation_object["authData"]
+        attestation_data = auth_data[37:]
+        credential_id_len = struct.unpack('!H', attestation_data[16:18])[0]
+        credential_id = attestation_data[18:18 + credential_id_len]
+
+        return credential_id
+
+
     def verify(self, existing_credential_ids=None):
         """
         Verify the WebAuthnRegistrationResponse.
 
         This will perform the registration ceremony for the
         WebAuthnRegistrationResponse. It will only return on successful
-        verification. In any other case, an appropriate error will be raised.
+        verification. In any other case, an appropriate error will
+        be raised.
 
-        :param existing_credential_ids: A list of existing credential ids to check for duplicates.
+        :param existing_credential_ids: A list of existing
+        credential ids to check for duplicates.
         :type existing_credential_ids: basestring[]
-        :return: The WebAuthnCredential produced by the registration ceremony.
-        :rtype: WebAuthnCredential
+        :return: The VerifiedRegistration produced by the registration
+        ceremony.
+        :rtype: VerifiedRegistration
         """
 
         try:
-            # As described in https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential
-            # In the docs it starts at step 5.
-            #
-            # Step 1.
-            #
-            # Let JSONtext be the result of running UTF-8 decode on the value of
-            # response.clientDataJSON
-            json_text = self.registration_response.get('clientData', '')
-
-            # Step 2.
-            #
-            # Let C, the client data claimed as collected during the credential
-            # creation, be the result of running an implementation-specific JSON
-            # parser on JSONtext.
-            decoded_cd = webauthn_b64_decode(json_text)
-            c = json.loads(to_unicode(decoded_cd))
-
-            # Step 3.
-            #
-            # Verify that the value of C.type is webauthn.create.
-            if not _verify_type(c.get('type'), CLIENT_DATA_TYPE.CREATE):
-                raise RegistrationRejectedException('Invalid type.')
+            credential_raw_id = self.extract_credential_id()
+            credential_id = bytes_to_base64url(credential_raw_id)
 
-            # Step 4.
-            #
-            # Verify that the value of C.challenge matches the challenge that was sent
-            # to the authenticator in the create() call.
-            if not _verify_challenge(c.get('challenge'), self.challenge):
-                raise RegistrationRejectedException('Unable to verify challenge.')
-
-            # Step 5.
-            #
-            # Verify that the value of C.origin matches the Relying Party's origin.
-            if not _verify_origin(c, self.origin):
-                raise RegistrationRejectedException('Unable to verify origin.')
-
-            # Step 6.
-            #
-            # Verify that the value of C.tokenBinding.status matches the state of
-            # Token Binding for the TLS connection over which the assertion was
-            # obtained. If Token Binding was used on that TLS connection, also verify
-            # that C.tokenBinding.id matches the base64url encoding of the Token
-            # Binding ID for the connection.
-
-            # Chrome does not currently supply token binding in the clientDataJSON
-            # if not _verify_token_binding_id(c):
-            #     raise RegistrationRejectedException('Unable to verify token binding ID.')
-
-            # Step 7.
-            #
-            # Compute the hash of response.clientDataJSON using SHA-256.
-            client_data_hash = _get_client_data_hash(decoded_cd)
-
-            # Step 8.
-            #
-            # Perform CBOR decoding on the attestationObject field of
-            # the AuthenticatorAttestationResponse structure to obtain
-            # the attestation statement format fmt, the authenticator
-            # data authData, and the attestation statement attStmt.
-            att_obj = self.parse_attestation_object(self.registration_response.get('attObj'))
-            att_stmt = att_obj.get('attStmt')
-            auth_data = att_obj.get('authData')
-            fmt = att_obj.get('fmt')
-            if not auth_data or len(auth_data) < 37:
-                raise RegistrationRejectedException('Auth data must be at least 37 bytes.')
-
-            # Step 9.
-            #
-            # Verify that the RP ID hash in authData is indeed the
-            # SHA-256 hash of the RP ID expected by the RP.
-            if not _verify_rp_id_hash(_get_auth_data_rp_id_hash(auth_data), self.rp_id):
-                raise RegistrationRejectedException('Unable to verify RP ID hash.')
-
-            # Step 10.
-            #
-            # Verify that the User Present bit of the flags in authData
-            # is set.
-            if not AuthenticatorDataFlags(auth_data).user_present:
-                raise RegistrationRejectedException('Malformed request received.')
-
-            # Step 11.
-            #
-            # If user verification is required for this registration, verify
-            # that the User Verified bit of the flags in authData is set.
-            if self.uv_required and not AuthenticatorDataFlags(auth_data).user_verified:
-                raise RegistrationRejectedException('Malformed request received.')
+            if existing_credential_ids and credential_id in existing_credential_ids:
+                raise RegistrationRejectedException('Credential already exists.')
 
-            # Step 12.
-            #
-            # Verify that the values of the client extension outputs in
-            # clientExtensionResults and the authenticator extension outputs
-            # in the extensions in authData are as expected, considering the
-            # client extension input values that were given as the extensions
-            # option in the create() call. In particular, any extension
-            # identifier values in the clientExtensionResults and the extensions
-            # in authData MUST be also be present as extension identifier values
-            # in the extensions member of options, i.e., no extensions are
-            # present that were not requested. In the general case, the meaning
-            # of "are as expected" is specific to the Relying Party and which
-            # extensions are in use.
-            if not _verify_authenticator_extensions(auth_data, self.expected_registration_authenticator_extensions):
-                raise RegistrationRejectedException('Unable to verify authenticator extensions.')
-            if not _verify_client_extensions(
-                self.registration_response.get('registrationClientExtensions'),
-                self.expected_registration_client_extensions
-            ):
-                raise RegistrationRejectedException('Unable to verify client extensions.')
+            response = {
+                "attestationObject": base64url_to_bytes(self.registration_response["attObj"]),
+                "clientDataJSON": base64url_to_bytes(self.registration_response["clientData"])
+            }
 
-            # Step 13.
-            #
-            # Determine the attestation statement format by performing
-            # a USASCII case-sensitive match on fmt against the set of
-            # supported WebAuthn Attestation Statement Format Identifier
-            # values. The up-to-date list of registered WebAuthn
-            # Attestation Statement Format Identifier values is maintained
-            # in the IANA registry of the same name.
-            if not _verify_attestation_statement_format(fmt):
-                raise RegistrationRejectedException('Unable to verify attestation statement format.')
+            raw_registration_response = {
+                # need to make id == base64url(rawID)
+                "rawId": credential_raw_id,
+                "id": credential_id,
+                "response": response
+            }
 
-            # Step 14.
-            #
-            # Verify that attStmt is a correct attestation statement, conveying
-            # a valid attestation signature, by using the attestation statement
-            # format fmt's verification procedure given attStmt, authData and
-            # the hash of the serialized client data computed in step 7.
-            (
-                attestation_type,
-                trust_path,
-                credential_public_key,
-                cred_id,
-                aaguid
-            ) = self.verify_attestation_statement(
-                fmt,
-                att_stmt,
-                auth_data,
-                client_data_hash,
-                self.none_attestation_permitted
+            verified_registration = verify_registration_response(
+                credential=RegistrationCredential.parse_obj(raw_registration_response),
+                expected_challenge=base64url_to_bytes(self.challenge),
+                expected_rp_id=self.rp_id,
+                expected_origin=self.origin,
+                require_user_verification=self.uv_required
             )
-            b64_cred_id = webauthn_b64_encode(cred_id)
-
-            # Step 15.
-            #
-            # If validation is successful, obtain a list of acceptable trust
-            # anchors (attestation root certificates or ECDAA-Issuer public
-            # keys) for that attestation type and attestation statement format
-            # fmt, from a trusted source or from policy. For example, the FIDO
-            # Metadata Service [FIDOMetadataService] provides one way to obtain
-            # such information, using the aaguid in the attestedCredentialData
-            # in authData.
-            trust_anchors = _get_trust_anchors(attestation_type, fmt, self.trust_anchor_dir) \
-                if self.trust_anchor_dir \
-                else None
-            if not trust_anchors and self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('No trust anchors available to verify attestation certificate.')
-
-            # Step 16.
-            #
-            # Assess the attestation trustworthiness using the outputs of the
-            # verification procedure in step 14, as follows:
-            #
-            #     * If self attestation was used, check if self attestation is
-            #       acceptable under Relying Party policy.
-            #     * If ECDAA was used, verify that the identifier of the
-            #       ECDAA-Issuer public key used is included in the set of
-            #       acceptable trust anchors obtained in step 15.
-            #     * Otherwise, use the X.509 certificates returned by the
-            #       verification procedure to verify that the attestation
-            #       public key correctly chains up to an acceptable root
-            #       certificate.
-            if attestation_type == ATTESTATION_TYPE.SELF_ATTESTATION and not self.self_attestation_permitted:
-                raise RegistrationRejectedException('Self attestation is not permitted.')
-            is_trusted_attestation_cert = ((attestation_type == ATTESTATION_TYPE.BASIC
-                                            and _is_trusted_x509_attestation_cert(trust_path, trust_anchors))
-                                           or (attestation_type == ATTESTATION_TYPE.ECDAA
-                                               and _is_trusted_ecdaa_attestation_certificate(None, trust_anchors)))
-            is_signed_attestation_cert = attestation_type in SUPPORTED_ATTESTATION_TYPES
-
-            if is_trusted_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.TRUSTED
-            elif is_signed_attestation_cert:
-                attestation_level = ATTESTATION_LEVEL.UNTRUSTED
-            else:
-                attestation_level = ATTESTATION_LEVEL.NONE
-
-            # 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.
-            if existing_credential_ids and b64_cred_id in existing_credential_ids:
-                raise RegistrationRejectedException('Credential already exists.')
+            return verified_registration
 
-            # Step 18.
-            #
-            # If the attestation statement attStmt verified successfully and is
-            # found to be trustworthy, then register the new credential with the
-            # account that was denoted in the options.user passed to create(),
-            # by associating it with the credentialId and credentialPublicKey in
-            # the attestedCredentialData in authData, as appropriate for the
-            # Relying Party's system.
-            credential = WebAuthnCredential(rp_id=self.rp_id,
-                                            origin=self.origin,
-                                            aaguid=aaguid,
-                                            credential_id=b64_cred_id,
-                                            public_key=webauthn_b64_encode(credential_public_key),
-                                            sign_count=struct.unpack('!I', auth_data[33:37])[0],
-                                            attestation_level=attestation_level,
-                                            attestation_cert=trust_path[0] if trust_path else None)
-            if is_trusted_attestation_cert:
-                return credential
-
-            # Step 19.
-            #
-            # If the attestation statement attStmt successfully verified but is
-            # not trustworthy per step 16 above, the Relying Party SHOULD fail
-            # the registration ceremony.
-            #
-            #     NOTE: However, if permitted by policy, the Relying Party MAY
-            #           register the credential ID and credential public key but
-            #           treat the credential as one with self attestation (see
-            #           6.3.3 Attestation Types). If doing so, the Relying Party
-            #           is asserting there is no cryptographic proof that the
-            #           public key credential has been generated by a particular
-            #           authenticator model. See [FIDOSecRef] and [UAFProtocol]
-            #           for a more detailed discussion.
-            if self.trusted_attestation_cert_required:
-                raise RegistrationRejectedException('Untrusted attestation certificate.')
-            if not is_signed_attestation_cert and not self.none_attestation_permitted:
-                raise RegistrationRejectedException('No (or unsupported) attestation certificate.')
-            return credential
-
-        except Exception as e:
-            raise RegistrationRejectedException('Registration rejected. Error: {}'.format(e))
+        except InvalidRegistrationResponse as ex:
+            raise RegistrationRejectedException from ex
 
 
 class WebAuthnAssertionResponse(object):
@@ -1816,7 +1587,7 @@ def _load_cose_public_key(key_bytes):
         y = int(codecs.encode(cose_public_key[COSE_PUBLIC_KEY.Y], 'hex'), 16)
 
         return alg, EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key(backend=default_backend())
-    elif alg in (COSE_ALGORITHM.PS256, COSE_ALGORITHM.RS256, COSE_ALGORITHM.RS1):
+    elif alg in (COSE_ALGORITHM.PS256, COSE_ALGORITHM.RS256):
 
         required_keys = {
             COSE_PUBLIC_KEY.ALG,
@@ -1835,7 +1606,6 @@ def _load_cose_public_key(key_bytes):
 
         return alg, RSAPublicNumbers(e, n).public_key(backend=default_backend())
     else:
-        log.warning('Unsupported webAuthn COSE algorithm: {0!s}'.format(alg))
         raise COSEKeyException('Unsupported algorithm.')
 
 
@@ -2047,7 +1817,5 @@ def _verify_signature(public_key, alg, data, signature):
     elif alg == COSE_ALGORITHM.PS256:
         padding = PSS(mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH)
         public_key.verify(signature, data, padding, SHA256())
-    elif alg == COSE_ALGORITHM.RS1:
-        public_key.verify(signature, data, PKCS1v15(), SHA1())
     else:
         raise NotImplementedError()
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
index b52f8a2b3dc80174f7d0a9fd7826b06e381a6c06..6f62b1994c25ba12d97af392f709e8702ab28dbc 100644
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -22,28 +22,35 @@
 #
 
 import binascii
+import logging
 
-from OpenSSL import crypto
-from cryptography import x509
+from webauthn import verify_authentication_response, base64url_to_bytes
+from webauthn.helpers import bytes_to_base64url
+from webauthn.helpers.exceptions import InvalidAuthenticationResponse
+from webauthn.helpers.structs import AuthenticationCredential
 
-from privacyidea.api.lib.utils import getParam, attestation_certificate_allowed
+from privacyidea.api.lib.utils import getParam
+from privacyidea.lib import _
 from privacyidea.lib.challenge import get_challenges
 from privacyidea.lib.config import get_from_config
 from privacyidea.lib.crypto import geturandom
 from privacyidea.lib.decorators import check_token_locked
-from privacyidea.lib.error import ParameterError, EnrollmentError, PolicyError
+from privacyidea.lib.error import ParameterError, RegistrationError
+from privacyidea.lib.log import log_with
+from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.tokenclass import TokenClass, CLIENTMODE, ROLLOUTSTATE
-from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM, webauthn_b64_encode, WebAuthnRegistrationResponse,
-                                             ATTESTATION_REQUIREMENT_LEVEL, webauthn_b64_decode,
-                                             WebAuthnMakeCredentialOptions, WebAuthnAssertionOptions, WebAuthnUser,
-                                             WebAuthnAssertionResponse, AuthenticationRejectedException,
-                                             USER_VERIFICATION_LEVEL)
 from privacyidea.lib.tokens.u2ftoken import IMAGES
-from privacyidea.lib.log import log_with
-import logging
-from privacyidea.lib import _
-from privacyidea.lib.policy import SCOPE, GROUP, ACTION
+from privacyidea.lib.tokens.webauthn import (COSE_ALGORITHM,
+                                             webauthn_b64_encode,
+                                             WebAuthnRegistrationResponse,
+                                             ATTESTATION_REQUIREMENT_LEVEL,
+                                             webauthn_b64_decode,
+                                             WebAuthnMakeCredentialOptions,
+                                             WebAuthnAssertionOptions,
+                                             WebAuthnUser,
+                                             AuthenticationRejectedException,
+                                             USER_VERIFICATION_LEVEL)
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import hexlify_and_unicode, is_true
 
@@ -77,69 +84,44 @@ The enrollment/registering can be completely performed within privacyIDEA.
 But if you want to enroll the WebAuthn token via the REST API you need to do
 it in two steps:
 
-**Step 1**
+Step 1
+~~~~~~
 
 .. sourcecode:: http
 
     POST /token/init HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
-
+    
     type=webauthn
-    user=<username>
+    
+This step returns a nonce, a relying party (containing a name and an ID
+generated from your domain), and a serial number, along with a transaction ID,
+and a message to display to the user. It will also pass some additional options
+regarding timeout, which authenticators are acceptable, and what key types are
+acceptable to the server.
 
-The request returns:
+Step 2
+~~~~~~
 
 .. sourcecode:: http
 
-    HTTP/1.1 200 OK
-    Content-Type: application/json
-
-    {
-        "detail": {
-            "serial": "<serial number>",
-            "webAuthnRegisterRequest": {
-                "attestation": "direct",
-                "authenticatorSelection": {
-                    "userVerification": "preferred"
-                },
-                "displayName": "<user.resolver@realm>",
-                "message": "Please confirm with your WebAuthn token",
-                "name": "<username>",
-                "nonce": "<nonce>",
-                "pubKeyCredAlgorithms": [
-                    {
-                        "alg": -7,
-                        "type": "public-key"
-                    },
-                    {
-                        "alg": -37,
-                        "type": "public-key"
-                    }
-                ],
-                "relyingParty": {
-                    "id": "<relying party ID>",
-                    "name": "<relying party name>"
-                },
-                "serialNumber": "<serial number>",
-                "timeout": 60000,
-                "transaction_id": "<transaction ID>"
-            }
-        },
-        "result": {
-            "status": true,
-            "value": true
-        },
-        "version": "<privacyIDEA version>"
-    }
+    POST /token/init HTTP/1.1
+    Host: example.com
+    Accept: application/json
+    
+    type=webauthn
+    transaction_id=<transaction_id>
+    description=<description>
+    clientdata=<clientDataJSON>
+    regdata=<attestationObject>
+    registrationclientextensions=<registrationClientExtensions>
 
-This step returns a *webAuthnRegisterRequest* which contains a nonce, a relying party (containing a
-name and an ID generated from your domain), a serial number along with a transaction ID
-and a message to display to the user. It will also contain some additional options
-regarding timeout, which authenticators are acceptable, and what key types are
-acceptable to the server.
+*clientDataJSON* and *attestationObject* are the values returned by the
+WebAuthn authenticator. *description* is an optional description string for 
+the new token.
 
-With the received data You need to call the javascript function
+You need to call the javascript function
 
 .. sourcecode:: javascript
 
@@ -153,7 +135,16 @@ With the received data You need to call the javascript function
                 name: <name>,
                 displayName: <displayName>
             },
-            pubKeyCredParams: <pubKeyCredAlgorithms>,
+            pubKeyCredParams: [
+                {
+                    alg: <preferredAlgorithm>,
+                    type: "public-key"
+                },
+                {
+                    alg: <alternativeAlgorithm>,
+                    type: "public-key"
+                }
+            ],
             authenticatorSelection: <authenticatorSelection>,
             timeout: <timeout>,
             attestation: <attestation>,
@@ -164,11 +155,11 @@ With the received data You need to call the javascript function
         .then(function(credential) { <responseHandler> })
         .catch(function(error) { <errorHandler> });
 
-Here *nonce*, *relyingParty*, *serialNumber*, *pubKeyCredAlgorithms*,
-*authenticatorSelection*, *timeout*, *attestation*,
+Here *nonce*, *relyingParty*, *serialNumber*, *preferredAlgorithm*,
+*alternativeAlgorithm*, *authenticatorSelection*, *timeout*, *attestation*,
 *authenticatorSelectionList*, *name*, and *displayName* are the values
 provided by the server in the *webAuthnRegisterRequest* field in the response
-from the first step. *authenticatorSelection*,
+from the first step. *alternativeAlgorithm*, *authenticatorSelection*,
 *timeout*, *attestation*, and *authenticatorSelectionList* are optional. If
 *attestation* is not provided, the client should default to `direct`
 attestation. If *timeout* is not provided, it may be omitted, or a sensible
@@ -185,38 +176,18 @@ company-provided token.
 
 The *responseHandler* needs to then send the *clientDataJSON*,
 *attestationObject*, and *registrationClientExtensions* contained in the
-*response* field of the *credential* back to the server. If
+*response* field of the *credential* (2. step) back to the server. If
 enrollment succeeds, the server will send a response with a
 *webAuthnRegisterResponse* field, containing a *subject* field with the
 description of the newly created token.
 
-
-**Step 2**
-
-.. sourcecode:: http
-
-    POST /token/init HTTP/1.1
-    Host: <privacyIDEA server>
-    Accept: application/json
-
-    type=webauthn
-    transaction_id=<transaction_id>
-    description=<description>
-    clientdata=<clientDataJSON>
-    regdata=<attestationObject>
-    registrationclientextensions=<registrationClientExtensions>
-
-The values *clientDataJSON* and *attestationObject* are returned by the
-WebAuthn authenticator. *description* is an optional description string for
-the new token.
-
 The server expects the *clientDataJSON* and *attestationObject* encoded as
 web-safe base64 as defined by the WebAuthn standard. This encoding is similar
 to standard base64, but '-' and '_' should be used in the alphabet instead of
 '+' and '/', respectively, and any padding should be omitted.
 
 The *registrationClientExtensions* are optional and should simply be omitted,
-if the client does not provide them. If the *registrationClientExtensions* are
+if the client does not provide them. It the *registrationClientExtensions* are
 available, they must be encoded as a utf-8 JSON string, then sent to the server
 as web-safe base64.
 
@@ -243,62 +214,53 @@ Get the challenge (using /validate/check)
 The /validate/check endpoint can be used to trigger a challenge using the PIN
 for the token (without requiring any special permissions).
 
-**Request:**
+**Request**
 
 .. sourcecode:: http
 
     POST /validate/check HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
-
+    
     user=<username>
     pass=<password>
-
-**Response:**
+    
+**Response**
 
 .. sourcecode:: http
 
     HTTP/1.1 200 OK
     Content-Type: application/json
-
+    
     {
         "detail": {
             "attributes": {
                 "hideResponseInput": true,
-                "img": "<image URL>",
+                "img": <imageUrl>,
                 "webAuthnSignRequest": {
-                    "allowCredentials": [
-                        {
-                            "id": "<credential ID>",
-                            "transports": [
-                                "<allowed transports>"
-                            ],
-                            "type": "<credential type>"
-                        }
-                    ],
-                    "challenge": "<nonce>",
-                    "rpId": "<relying party ID>",
-                    "timeout": 60000,
-                    "userVerification": "<user verification requirement>"
+                    "challenge": <nonce>,
+                    "allowCredentials": [{
+                        "id": <credentialId>,
+                        "type": <credentialType>,
+                        "transports": <allowedTransports>,
+                    }],
+                    "rpId": <relyingPartyId>,
+                    "userVerification": <userVerificationRequirement>,
+                    "timeout": <timeout>
                 }
             },
-            "client_mode": "webauthn",
             "message": "Please confirm with your WebAuthn token",
-            "serial": "<token serial>",
-            "transaction_id": "<transaction ID>",
-            "type": "webauthn"
+            "transaction_id": <transactionId>
         },
         "id": 1,
         "jsonrpc": "2.0",
         "result": {
-            "authentication": "CHALLENGE",
             "status": true,
             "value": false
         },
-        "version": "<privacyIDEA version>"
+        "versionnumber": <privacyIDEAversion>
     }
 
-
 Get the challenge (using /validate/triggerchallenge)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -310,13 +272,13 @@ using a service account (without requiring the PIN for the token).
 .. sourcecode:: http
 
     POST /validate/triggerchallenge HTTP/1.1
-    Host: <privacyIDEA server>
+    Host: example.com
     Accept: application/json
     PI-Authorization: <authToken>
-
+    
     user=<username>
     serial=<tokenSerial>
-
+    
 Providing the *tokenSerial* is optional. If just a user is provided, a
 challenge will be triggered for every challenge response token the user has.
 
@@ -326,24 +288,23 @@ challenge will be triggered for every challenge response token the user has.
 
     HTTP/1.1 200 OK
     Content-Type: application/json
-
+    
+    
     {
         "detail": {
             "attributes": {
                 "hideResponseInput": true,
-                "img": "<image URL>",
+                "img": <imageUrl>,
                 "webAuthnSignRequest": {
-                    "challenge": "<nonce>",
+                    "challenge": <nonce>,
                     "allowCredentials": [{
-                        "id": "<credential ID>",
-                        "transports": [
-                            "<allowed transports>"
-                        ],
-                        "type": "<credential type>",
+                        "id": <credentialId>,
+                        "type": <credentialType>,
+                        "transports": <allowedTransports>,
                     }],
-                    "rpId": "<relying party ID>",
-                    "userVerification": "<user verification requirement>",
-                    "timeout": 60000
+                    "rpId": <relyingPartyId>,
+                    "userVerification": <userVerificationRequirement>,
+                    "timeout": <timeout>
                 }
             },
             "message": "Please confirm with your WebAuthn token",
@@ -351,29 +312,28 @@ challenge will be triggered for every challenge response token the user has.
             "multi_challenge": [{
                 "attributes": {
                     "hideResponseInput": true,
-                    "img": "<image URL>",
+                    "img": <imageUrl>,
                     "webAuthnSignRequest": {
-                        "challenge": "<nonce>",
+                        "challenge": <nonce>,
                         "allowCredentials": [{
-                            "id": "<credential ID>",
-                            "transports": [
-                                "<allowedTransports>"
-                            ],
-                            "type": "<credential type>",
+                            "id": <credentialId>,
+                            "type": <credentialType>,
+                            "transports": <allowedTransports>,
                         }],
-                        "rpId": "<relying party ID>",
-                        "userVerification": "<user verification requirement>",
-                        "timeout": 60000
+                        "rpId": <relyingPartyId>,
+                        "userVerification": <userVerificationRequirement>,
+                        "timeout": <timeout>
                     }
                 },
                 "message": "Please confirm with your WebAuthn token",
-                "serial": "<token serial>",
-                "transaction_id": "<transaction ID>",
+                "serial": <tokenSerial>,
+                "transaction_id": <transactionId>,
                 "type": "webauthn"
             }],
-            "serial": "<token serial>",
-            "transaction_id": "<transaction ID>",
-            "transaction_ids": ["<transaction IDs>"],
+            "serial": <tokenSerial>,
+            "threadid": <threadId>,
+            "transaction_id": <transactionId>,
+            "transaction_ids": [<transactionId>],
             "type": "webauthn"
         },
         "id": 1,
@@ -382,15 +342,15 @@ challenge will be triggered for every challenge response token the user has.
             "status": true,
             "value": 1
         },
-        "version": "<privacyIDEA version>"
+        "versionnumber": <privacyIDEAversion>
     }
-
+    
 Send the Response
 ~~~~~~~~~~~~~~~~~
 
 The application now needs to call the javascript function
-*navigator.credentials.get* with the *publicKeyCredentialRequestOptions* built
-using the *nonce*, *credentialId*, *allowedTransports*, *userVerificationRequirement*
+*navigator.credentials.get* with *publicKeyCredentialRequestOptions* built using
+the *nonce*, *credentialId*, *allowedTransports*, *userVerificationRequirement*
 and *timeout* from the server.  The timeout is optional and may be omitted, if
 not provided, the client may also pick a sensible default. Please note that the
 nonce will be a binary, encoded using the web-safe base64 algorithm specified by
@@ -440,7 +400,7 @@ native encoding of the language (usually utf-16).
     POST /validate/check HTTP/1.1
     Host: example.com
     Accept: application/json
-
+    
     user=<user>
     pass=
     transaction_id=<transaction_id>
@@ -533,6 +493,12 @@ class WEBAUTHNINFO(object):
     ATTESTATION_SUBJECT = "attestation_subject"
     RELYING_PARTY_ID = "relying_party_id"
     RELYING_PARTY_NAME = "relying_party_name"
+    FORMAT = "format"
+    CREDENTIAL_TYPE = "credential_type"
+    USER_VERIFIED = "user_verified"
+    ATTESTATION_OBJECT = "attestation_object"
+    CREDENTIAL_DEVICE_TYPE = "credential_device_type"
+    CREDENTIAL_BACKED_UP = "credential_backed_up"
 
 
 class WEBAUTHNGROUP(object):
@@ -697,10 +663,15 @@ class WebAuthnTokenClass(TokenClass):
                     },
                     WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: {
                         'type': 'str',
-                        'desc': _("Which algorithm are available to use for creating public key "
-                                  "credentials for WebAuthn tokens. (Default: [{0!s}], Order: "
-                                  "[{1!s}]".format(', '.join(DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE),
-                                                   ', '.join(PUBKEY_CRED_ALGORITHMS_ORDER))),
+                        'desc': _(
+                            "Which algorithm are available to use for "
+                            "creating public key "
+                            "credentials for WebAuthn tokens. (Default: [{"
+                            "0!s}], Order: "
+                            "[{1!s}]".format(', '.join(
+                                DEFAULT_PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE),
+                                             ', '.join(
+                                                 PUBKEY_CRED_ALGORITHMS_ORDER))),
                         'group': WEBAUTHNGROUP.WEBAUTHN,
                         'multiple': True,
                         'value': list(PUBLIC_KEY_CREDENTIAL_ALGORITHMS.keys())
@@ -807,7 +778,7 @@ class WebAuthnTokenClass(TokenClass):
             user_display_name=str(user),
             icon_url=IMAGES.get(self.token.description.lower().split()[0], "") if self.token.description else "",
             credential_id=self.decrypt_otpkey(),
-            public_key=webauthn_b64_encode(binascii.unhexlify(self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY))),
+            public_key=self.get_tokeninfo(WEBAUTHNINFO.PUB_KEY),
             sign_count=self.get_otp_count(),
             rp_id=self.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID)
         )
@@ -826,7 +797,7 @@ class WebAuthnTokenClass(TokenClass):
         :rtype: basestring
         """
 
-        return webauthn_b64_encode(binascii.unhexlify(self.token.get_otpkey().getKey()))
+        return webauthn_b64_encode(self.token.get_otpkey().getKey())
 
     def update(self, param, reset_failcount=True):
         """
@@ -874,7 +845,7 @@ class WebAuthnTokenClass(TokenClass):
 
             # Since we are still enrolling the token, there should be exactly one challenge.
             if not len(challengeobject_list):
-                raise EnrollmentError(
+                raise RegistrationError(
                     "The enrollment challenge does not exist or has timed out for {0!s}".format(serial))
             challengeobject = challengeobject_list[0]
             challenge = binascii.unhexlify(challengeobject.challenge)
@@ -883,55 +854,46 @@ class WebAuthnTokenClass(TokenClass):
             #
             # All data is parsed and verified. If any errors occur an exception
             # will be raised.
-            try:
-                webauthn_credential = WebAuthnRegistrationResponse(
-                    rp_id=rp_id,
-                    origin=http_origin,
-                    registration_response={
-                        'clientData': client_data,
-                        'attObj': reg_data,
-                        'registrationClientExtensions':
-                            webauthn_b64_decode(registration_client_extensions)
+            verified_registration = WebAuthnRegistrationResponse(
+                rp_id=rp_id,
+                origin=http_origin,
+                registration_response={
+                    'clientData': client_data,
+                    'attObj': reg_data,
+                    'registrationClientExtensions':
+                        webauthn_b64_decode(registration_client_extensions)
                             if registration_client_extensions
                             else None
-                    },
-                    challenge=webauthn_b64_encode(challenge),
-                    attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[attestation_level],
-                    trust_anchor_dir=get_from_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR),
-                    uv_required=uv_req == USER_VERIFICATION_LEVEL.REQUIRED
-                ).verify([
-                    # TODO: this might get slow when a lot of webauthn tokens are registered
-                    token.decrypt_otpkey() for token in get_tokens(tokentype=self.type) if token.get_serial() != self.get_serial()
-                ])
-            except Exception as e:
-                log.warning('Enrollment of {0!s} token failed: '
-                            '{1!s}!'.format(self.get_class_type(), e))
-                raise EnrollmentError("Could not enroll {0!s} token!".format(self.get_class_type()))
-
-            self.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(webauthn_credential.credential_id)))
-            self.set_otp_count(webauthn_credential.sign_count)
+                },
+                challenge=webauthn_b64_encode(challenge),
+                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[attestation_level],
+                trust_anchor_dir=get_from_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR),
+                uv_required=uv_req == USER_VERIFICATION_LEVEL.REQUIRED
+            ).verify([
+                token.decrypt_otpkey() for token in get_tokens(tokentype=self.type)
+            ])
+
+            self.set_otpkey(webauthn_b64_decode(bytes_to_base64url(verified_registration.credential_id)))
+            self.set_otp_count(verified_registration.sign_count)
+            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN, http_origin)
+            self.add_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID, rp_id)
             self.add_tokeninfo(WEBAUTHNINFO.PUB_KEY,
-                               hexlify_and_unicode(webauthn_b64_decode(webauthn_credential.public_key)))
-            self.add_tokeninfo(WEBAUTHNINFO.ORIGIN,
-                               webauthn_credential.origin)
-            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_LEVEL,
-                               webauthn_credential.attestation_level)
-
+                                   bytes_to_base64url(
+                                       verified_registration.credential_public_key))
             self.add_tokeninfo(WEBAUTHNINFO.AAGUID,
-                               hexlify_and_unicode(webauthn_credential.aaguid))
-
-            # Add attestation info.
-            if webauthn_credential.attestation_cert:
-                # attestation_cert is of type cryptography.x509.Certificate.
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER,
-                                   webauthn_credential.attestation_cert.issuer.rfc4514_string())
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT,
-                                   webauthn_credential.attestation_cert.subject.rfc4514_string())
-                self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL,
-                                   webauthn_credential.attestation_cert.serial_number)
-
-                cn = webauthn_credential.attestation_cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
-                automatic_description = cn[0].value if len(cn) else None
+                               verified_registration.aaguid)
+            self.add_tokeninfo(WEBAUTHNINFO.FORMAT,
+                               verified_registration.fmt)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_TYPE,
+                               verified_registration.credential_type)
+            self.add_tokeninfo(WEBAUTHNINFO.USER_VERIFIED,
+                               verified_registration.user_verified)
+            self.add_tokeninfo(WEBAUTHNINFO.ATTESTATION_OBJECT,
+                               bytes_to_base64url(verified_registration.attestation_object))
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_DEVICE_TYPE,
+                               verified_registration.credential_device_type)
+            self.add_tokeninfo(WEBAUTHNINFO.CREDENTIAL_BACKED_UP,
+                               verified_registration.credential_device_type)
 
             # If no description has already been set, set the automatic description or the
             # description given in the 2nd request
@@ -1040,6 +1002,9 @@ class WebAuthnTokenClass(TokenClass):
                 "name": public_key_credential_creation_options["user"]["name"],
                 "displayName": public_key_credential_creation_options["user"]["displayName"]
             }
+            if len(public_key_credential_creation_options.get("pubKeyCredParams")) > 1:
+                response_detail["webAuthnRegisterRequest"]["alternativeAlgorithm"] \
+                    = public_key_credential_creation_options["pubKeyCredParams"][1]
             if public_key_credential_creation_options.get("authenticatorSelection"):
                 response_detail["webAuthnRegisterRequest"]["authenticatorSelection"] \
                     = public_key_credential_creation_options["authenticatorSelection"]
@@ -1182,7 +1147,7 @@ class WebAuthnTokenClass(TokenClass):
                       "image": user.icon_url}
 
         return True, message, db_challenge.transaction_id, reply_dict
-
+    
     @check_token_locked
     def check_otp(self, otpval, counter=None, window=None, options=None):
         """
@@ -1204,6 +1169,8 @@ class WebAuthnTokenClass(TokenClass):
         :return: A numerical value where values larger than zero indicate success.
         :rtype: int
         """
+        if self.get_tokeninfo("migrated") is not None:
+            return -1
 
         if is_webauthn_assertion_response(options) and getParam(options, "challenge", optional):
             credential_id = getParam(options, "credentialid", required)
@@ -1211,82 +1178,48 @@ class WebAuthnTokenClass(TokenClass):
             client_data = getParam(options, "clientdata", required)
             signature_data = getParam(options, "signaturedata", required)
             user_handle = getParam(options, "userhandle", optional)
-            assertion_client_extensions = getParam(options, "assertionclientextensions", optional)
+            uv_required = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
 
             try:
                 user = self._get_webauthn_user(getParam(options, "user", required))
             except ParameterError:
                 raise ValueError("When performing WebAuthn authorization, options must contain user")
 
-            uv_req = getParam(options, WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT, optional)
-
-            challenge = binascii.unhexlify(getParam(options, "challenge", required))
-
             try:
-                try:
-                    http_origin = getParam(options, "HTTP_ORIGIN", required, allow_empty=False)
-                except ParameterError:
-                    raise AuthenticationRejectedException('HTTP Origin header missing.')
-
-                # This does the heavy lifting.
-                #
-                # All data is parsed and verified. If any errors occur, an exception
-                # will be raised.
-                self.set_otp_count(WebAuthnAssertionResponse(
-                                       webauthn_user=user,
-                                       assertion_response={
-                                           'id': credential_id,
-                                           'userHandle': user_handle,
-                                           'clientData': client_data,
-                                           'authData': authenticator_data,
-                                           'signature': signature_data,
-                                           'assertionClientExtensions':
-                                               webauthn_b64_decode(assertion_client_extensions)
-                                                   if assertion_client_extensions
-                                                   else None
-                                       },
-                                       challenge=webauthn_b64_encode(challenge),
-                                       origin=http_origin,
-                                       allow_credentials=[user.credential_id],
-                                       uv_required=uv_req
-                                   ).verify())
-            except AuthenticationRejectedException as e:
-                # The authentication ceremony failed.
-                log.warning("Checking response for token {0!s} failed. {1!s}".format(self.token.serial, e))
-                return -1
-
-            # At this point we can check, if the attestation certificate is
-            # authorized. If not, we can raise a policy exception.
-            if not attestation_certificate_allowed(
-                    {
-                        "attestation_issuer": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_ISSUER),
-                        "attestation_serial": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SERIAL),
-                        "attestation_subject": self.get_tokeninfo(WEBAUTHNINFO.ATTESTATION_SUBJECT)
-                    },
-                    getParam(options, WEBAUTHNACTION.REQ, optional)
-            ):
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate "
-                    "due to policy restriction {1!s}".format(self.token.serial, WEBAUTHNACTION.REQ))
-                raise PolicyError("The WebAuthn token is not allowed to "
-                                  "authenticate due to a policy restriction.")
-
-            # Now we need to check, if a whitelist for AAGUIDs exists, and if
-            # so, if this device is whitelisted. If not, we again raise a
-            # policy exception.
-            allowed_aaguids = getParam(options, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST, optional)
-            if allowed_aaguids and self.get_tokeninfo(WEBAUTHNINFO.AAGUID) not in allowed_aaguids:
-                log.warning(
-                    "The WebAuthn token {0!s} is not allowed to authenticate due to policy "
-                    "restriction {1!s}".format(self.token.serial, WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST))
-                raise PolicyError("The WebAuthn token is not allowed to "
-                                  "authenticate due to a policy restriction.")
-
-            # All clear? Nice!
-            return self.get_otp_count()
+                auth_id = credential_id
+                auth_raw_id = base64url_to_bytes(auth_id)
+
+                response = {
+                    "authenticatorData": webauthn_b64_decode(authenticator_data),
+                    "clientDataJSON": webauthn_b64_decode(client_data),
+                    "signature": webauthn_b64_decode(signature_data),
+                    "userHandle": user_handle
+                }
+                raw_auth_response = {
+                    "rawId": auth_raw_id,
+                    "id": auth_id,
+                    "response": response
+                }
 
+                verified_authentication = verify_authentication_response(
+                    credential=AuthenticationCredential.parse_obj(
+                        raw_auth_response),
+                    expected_challenge=binascii.unhexlify(options["challenge"]),
+                    expected_rp_id=user.rp_id,
+                    expected_origin=options["HTTP_ORIGIN"],
+                    credential_public_key=base64url_to_bytes(user.public_key),
+                    credential_current_sign_count=user.sign_count,
+                    require_user_verification=uv_required
+                )
+                sign_count = verified_authentication.new_sign_count
+                self.set_otp_count(sign_count)
+
+                return sign_count
+
+            except InvalidAuthenticationResponse as ex:
+                log.warning(ex)
+                return -1
         else:
-            # Not all necessary data provided.
             return -1
 
 
diff --git a/privacyidea/models.py b/privacyidea/models.py
index 35dda42b38891023e64937484e198a88de3c5d82..3e78a7945dfbeaf4c954571e980c1f4c81b3b0ce 100644
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -107,10 +107,10 @@ def save_config_timestamp(invalidate_config=True):
     """
     c1 = Config.query.filter_by(Key=PRIVACYIDEA_TIMESTAMP).first()
     if c1:
-        c1.Value = datetime.now().strftime("%s")
+        c1.Value = datetime.now().strftime("%S")
     else:
         new_timestamp = Config(PRIVACYIDEA_TIMESTAMP,
-                               datetime.now().strftime("%s"),
+                               datetime.now().strftime("%S"),
                                Description="config timestamp. last changed.")
         db.session.add(new_timestamp)
     if invalidate_config:
diff --git a/privacyidea/static/components/token/views/token.enroll.backupcode.html b/privacyidea/static/components/token/views/token.enroll.backupcode.html
new file mode 100644
index 0000000000000000000000000000000000000000..0fcc96e502ba9b231e0d65629bf7b1c398f1b3c9
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enroll.backupcode.html
@@ -0,0 +1,5 @@
+<p class="help-block" translate>
+    The TAN token will let you print a list of OTP values.
+    These OTP values can be used to authenticate. The values can be used in an
+    arbitrary order.
+</p>
\ No newline at end of file
diff --git a/privacyidea/static/components/token/views/token.enrolled.backupcode.html b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
new file mode 100644
index 0000000000000000000000000000000000000000..7c1889a48fbb1db5ed3e7060a817ea87ef35bcdf
--- /dev/null
+++ b/privacyidea/static/components/token/views/token.enrolled.backupcode.html
@@ -0,0 +1,58 @@
+<div class="row">
+    <div class="col-sm-12">
+        <uib-accordion close-others="oneAtATime">
+            <div uib-accordion-group
+                 class="panel-default"
+                 heading="{{ 'The OTP values'|translate }}">
+                <div id="paperOtpTable">
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.top.html'"></div>
+                    <div class="table-responsive">
+                        <table class="table table-bordered table-striped
+                                  tantoken">
+                            <thead>
+                            <tr>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                                <th translate>#</th>
+                                <th>OTP</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr ng-repeat="key in otp_rows">
+                                <td>{{ $index }}</td>
+                                <td>{{ enrolledToken.otps[$index] }}</td>
+                                <td>{{ $index+(1*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (1*otp_row_count)] }}</td>
+                                <td>{{ $index+(2*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (2*otp_row_count)] }}</td>
+                                <td>{{ $index+(3*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (3*otp_row_count)] }}</td>
+                                <td>{{ $index+(4*otp_row_count) }}</td>
+                                <td>{{ enrolledToken.otps[$index+
+                                        (4*otp_row_count)] }}</td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                    <div ng-include="instanceUrl+'/'+piCustomization+
+                    '/views/includes/token.enrolled.tan.bottom.html'"></div>
+                </div>
+            </div>
+        </uib-accordion>
+    </div>
+</div>
+<button class="btn-default btn"
+    ng-click="printOtp()">
+    <span class="glyphicon glyphicon-print"></span>
+    <span translate>Print the OTP list</span>
+</button>
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 0eeb52bac6d6d14baa6d71a6c308fcc3998f7d6a..df3f0a48e4ea481589f51de0e7e2e4f0bde2ec9b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -106,3 +106,4 @@ zipp==3.8.0; python_version > '3.6'
 grpcio==1.46.0; python_version >= '3.7'
 grpcio-tools==1.46.0; python_version >= '3.7'
 protobuf==3.20.2; python_version >= '3.7'
+webauthn~=1.6
diff --git a/setup.py b/setup.py
index 2019498435c0e1a6367863286b4b0def52f6bd65..1a4d2b5dde1c2ba8b32ad2ea9050d1d88c990eec 100644
--- a/setup.py
+++ b/setup.py
@@ -62,6 +62,7 @@ install_requires = ["beautifulsoup4[lxml]>=4.3.2",
                     "segno>=1.5",
                     "smpplib>=2.0",
                     "SQLAlchemy>=1.3.0,<1.4.0",
+                    "webauthn~=1.6",
                     "sqlsoup>=0.9.0"]
 
 
diff --git a/tests/test_lib_tokens_backupcode.py b/tests/test_lib_tokens_backupcode.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e3103befc018f93ce4459f2719e87e761e8e693
--- /dev/null
+++ b/tests/test_lib_tokens_backupcode.py
@@ -0,0 +1,150 @@
+"""
+This test file tests the lib.tokens.backupcodetoken
+This depends on lib.tokenclass
+"""
+import ast
+
+from privacyidea.lib.token import init_token
+from privacyidea.lib.tokens.backupcodetoken import BackupCodeTokenClass
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_COUNT
+from privacyidea.lib.tokens.backupcodetoken import DEFAULT_LENGTH
+from privacyidea.models import Token
+from .base import MyTestCase
+
+"""
+ Tokens need to be of configured length in order for pin-parsing to work.
+ Pin and otp value are passed as a single string which is then split based on
+ the length of the OTP.
+"""
+VALID_OTP_1 = "a" * DEFAULT_LENGTH
+VALID_OTP_2 = "b" * DEFAULT_LENGTH
+INVALID_OTP = "n" * DEFAULT_LENGTH
+
+VALID_PIN = "valid_pin"
+INVALID_PIN = "invalid_pin"
+
+SUCCESS = 1
+FAILURE = -1
+
+
+class BackupCodeTokenTestCase(MyTestCase):
+    serial1 = "ser1"
+
+    def get_fresh_token(self, set_pin=False):
+        token = init_token({"type": "backupcode"})
+        token.set_tokeninfo(
+            {"otps": f"['{VALID_OTP_1}', '{VALID_OTP_2}']", "used_otps": "[]"})
+
+        if set_pin:
+            token.set_pin(VALID_PIN)
+
+        return token
+
+    def test_01_create_token(self):
+        db_token = Token(self.serial1, tokentype="backupcode")
+        db_token.save()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        self.assertEqual(token.token.serial, self.serial1)
+        self.assertEqual(token.token.tokentype, "backupcode")
+        self.assertEqual(token.type, "backupcode")
+
+        class_prefix = token.get_class_prefix()
+
+        self.assertEqual(class_prefix, "BUC")
+        self.assertEqual(token.get_class_type(), "backupcode")
+
+    def test_02_class_methods(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+
+        info = token.get_class_info()
+        self.assertEqual(info.get("title"), "Backup Code Token")
+
+        info = token.get_class_info("title")
+        self.assertEqual(info, "Backup Code Token")
+
+    def test_03_get_init_details(self):
+        db_token = Token.query.filter(Token.serial == self.serial1).first()
+        token = BackupCodeTokenClass(db_token)
+        token.update({})
+
+        # make sure OTPs were created
+        init_detail = token.get_init_detail()
+        self.assertTrue("otps" in init_detail)
+
+        # make sure there's correct (configured) number of generated OTPs
+        frontend_otps = init_detail.get("otps")
+        backend_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+        self.assertEqual(frontend_otps, backend_otps)
+        self.assertTrue(len(frontend_otps) == DEFAULT_COUNT)
+
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+        self.assertEqual(used_otps, [])
+
+        # make sure the generated OTPs are of correct (configured) length
+        for otp in frontend_otps:
+            self.assertTrue(len(otp) == DEFAULT_LENGTH)
+
+    def test_04_check_unused_existent_otp(self):
+        token = self.get_fresh_token()
+        valid_otp_check_result = token.check_otp(VALID_OTP_1)
+        used_otps = ast.literal_eval(token.get_tokeninfo("used_otps"))
+
+        self.assertEquals(valid_otp_check_result, SUCCESS)
+        self.assertEquals(used_otps, [VALID_OTP_1])
+
+    def test_05_check_used_existent_otp(self):
+        token = self.get_fresh_token()
+
+        self.assertEquals(token.check_otp(VALID_OTP_1), SUCCESS)
+        self.assertEquals(token.check_otp(VALID_OTP_1), FAILURE)
+
+    def test_06_check_non_existent_otp(self):
+        token = self.get_fresh_token()
+        all_otps = ast.literal_eval(token.get_tokeninfo("otps"))
+
+        self.assertTrue(INVALID_OTP not in all_otps)
+        self.assertEquals(token.check_otp(INVALID_OTP), FAILURE)
+
+    def test_07_authenticate_with_correct_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_with_incorrect_pin_and_correct_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = INVALID_PIN + VALID_OTP_1
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertFalse(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_with_correct_pin_and_incorrect_otp(self):
+        token = self.get_fresh_token(set_pin=True)
+
+        auth_info = VALID_PIN + INVALID_OTP
+        is_pin_correct, otp_auth_result, _ = token.authenticate(auth_info)
+
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
+
+    def test_08_authenticate_without_pin_correct_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(VALID_OTP_1)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, SUCCESS)
+
+    def test_08_authenticate_without_pin_incorrect_otp(self):
+        token = self.get_fresh_token()
+
+        is_pin_correct, otp_auth_result, _ = token.authenticate(INVALID_OTP)
+        self.assertTrue(is_pin_correct)
+        self.assertEqual(otp_auth_result, FAILURE)
diff --git a/tests/test_lib_tokens_webauthn.py b/tests/test_lib_tokens_webauthn.py
index a795f87df61701722a2841a9950a83937eb1ef94..29b40ff693fb522c0806305323be649d16f2aa23 100644
--- a/tests/test_lib_tokens_webauthn.py
+++ b/tests/test_lib_tokens_webauthn.py
@@ -62,7 +62,6 @@ import struct
 import unittest
 from copy import copy
 from mock import patch
-from testfixtures import log_capture
 from privacyidea.lib.user import User
 
 from privacyidea.lib.config import set_privacyidea_config
@@ -80,7 +79,6 @@ from privacyidea.lib.tokens.webauthntoken import (WebAuthnTokenClass, WEBAUTHNAC
 from privacyidea.lib.token import init_token, check_user_pass, remove_token
 from privacyidea.lib.policy import set_policy, SCOPE, ACTION, delete_policy
 from privacyidea.lib.challenge import get_challenges
-from privacyidea.lib.error import PolicyError, ParameterError, EnrollmentError
 
 TRUST_ANCHOR_DIR = "{}/testdata/trusted_attestation_roots".format(os.path.abspath(os.path.dirname(__file__)))
 REGISTRATION_RESPONSE_TMPL = {
@@ -116,12 +114,11 @@ CRED_KEY = {
     'alg': -7,
     'type': 'public-key'
 }
-CREDENTIAL_ID = b'ilNaaY5fYJoR1sg5IB7FL2Zoa-qBd_5Q95ZcyxNkmjkoDhiLCLgEKoKfCUElLt6_6Dmj_EUuOHZUI6x_gC32LQ'
 REGISTRATION_CHALLENGE = 'bPzpX3hHQtsp9evyKYkaZtVc9UN07PUdJ22vZUdDp94'
 ASSERTION_CHALLENGE = 'e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw'
 RP_ID = "webauthn.io"
 ORIGIN = "https://webauthn.io"
-USER_NAME = 'hans'
+USER_NAME = 'testuser'
 ICON_URL = "https://example.com/icon.png"
 USER_DISPLAY_NAME = "A Test User"
 USER_ID = b'\x80\xf1\xdc\xec\xb5\x18\xb1\xc8b\x05\x886\xbc\xdfJ\xdf'
@@ -171,47 +168,11 @@ NONE_ATTESTATION_CRED_ID = 'XyBvPatPc3biviiMX2Rkq5Vj-W6Pk6RtNi6r7v0dSgrLYaPxxWi0
 NONE_ATTESTATION_PUB_KEY = 'a5010203262001215820d210be8ddfb5b0bc0be1ea8deaedec197e43e1fece4eb95791e97e1d9219491f22582'\
                            '0f80e408b103d424808474999556a4e2c76453cfd114295a39bc9325a83b84416'
 
-SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL = {
-    "clientData": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjaGFsbGVuZ2Ui"
-                  "OiJBWGtYV1hQUDNnTHg4T0xscGtKM2FSUmhGV250blNFTmdnbmpEcEJxbDFu"
-                  "Z0tvbDd4V3dldlVZdnJwQkRQM0xFdmRyMkVPU3RPRnBHR3huTXZYay1WdyIs"
-                  "InR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
-    "attObj": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzn__mNzaWdZAQCPypMLXWqtCZ1sc5Qd"
-              "jhH-pAzm8-adpfbemd5zsym2krscwV0EeOdTrdUOdy3hWj5HuK9dIX_OpNro2jKr"
-              "HfUj_0Kp-u87iqJ3MPzs-D9zXOqkbWqcY94Zh52wrPwhGfJ8BiQp5T4Q97E042hY"
-              "QRDKmtv7N-BT6dywiuFHxfm1sDbUZ_yyEIN3jgttJzjp_wvk_RJmb78bLPTlym83"
-              "Y0Ws73K6FFeiqFNqLA_8a4V0I088hs_IEPlj8PWxW0wnIUhI9IcRf0GEmUwTBpbN"
-              "DGpIFGOudnl_C3YuXuzK3R6pv2r7m9-9cIIeeYXD9BhSMBQ0A8oxBbVF7j-0xXDN"
-              "rXHZaGF1dGhEYXRhWQFnSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NB"
-              "AAAAOKjVmSRjt0nqud40p1PeHgEAIB-l9gZ544Ds7vzo_O76UZ8DCXiWFc8DN8LW"
-              "NZYQH0NepAEDAzn__iBZAQDAIqzybPPmgeL5OR6JKq9bWDiENJlN_LePQEnf1_sg"
-              "Om4FJ9kBTbOTtWplfoMXg40A7meMppiRqP72A3tmILwZ5xKIyY7V8Y2t8X1ilYJo"
-              "l2nCKOpAEqGLTRJjF64GQxen0uFpi1tA6l6N-ZboPxjky4aidBdUP22YZuEPCO8-"
-              "9ZTha8qwvTgZwMHhZ40TUPEJGGWOnHNlYmqnfFfk0P-UOZokI0rqtqqQGMwzV2Rr"
-              "H2kjKTZGfyskAQnrqf9PoJkye4KUjWkWnZzhkZbrDoLyTEX2oWvTTflnR5tAVMQc"
-              "h4UGgEHSZ00G5SFoc19nGx_UJcqezx5cLZsny-qQYDRjIUMBAAE"
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_BAD_COSE_ALG = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:529]
-              + 'y' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][530:]
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_ALG_MISMATCH = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:37]
-              + '2' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][38:]
-}
-
-SELF_ATTESTATION_REGISTRATION_RESPONSE_BROKEN_SIG = {
-    'clientData': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['clientData'],
-    'attObj': SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][:46]
-              + 'AA' + SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL['attObj'][48:]
-}
-
 
 class WebAuthnTokenTestCase(MyTestCase):
+    USER_LOGIN = "testuser"
+    USER_REALM = "testrealm"
+    USER_RESOLVER = "testresolver"
 
     def _create_challenge(self):
         self.token.set_otpkey(hexlify_and_unicode(webauthn_b64_decode(CRED_ID)))
@@ -220,37 +181,11 @@ class WebAuthnTokenTestCase(MyTestCase):
         (_, _, _, response_details) = self.token.create_challenge(options=self.challenge_options)
         return response_details
 
-    def _setup_token(self):
-        with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
-            mock_nonce.return_value = webauthn_b64_decode(REGISTRATION_CHALLENGE)
-            self.token.get_init_detail(self.init_params, self.user)
-        self.token.update({
-            'type': 'webauthn',
-            'serial': self.token.token.serial,
-            'regdata': REGISTRATION_RESPONSE_TMPL['attObj'],
-            'clientdata': REGISTRATION_RESPONSE_TMPL['clientData'],
-            WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
-            WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_LEVEL: ATTESTATION_LEVEL.NONE,
-            'HTTP_ORIGIN': ORIGIN
-        })
-
     def setUp(self):
-        self.setUp_user_realms()
-
-        set_policy(name="WebAuthn",
-                   scope=SCOPE.ENROLL,
-                   action=WEBAUTHNACTION.RELYING_PARTY_NAME + "=" + RP_NAME + ","
-                          + WEBAUTHNACTION.RELYING_PARTY_ID + "=" + RP_ID)
-        set_privacyidea_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR, TRUST_ANCHOR_DIR)
-        set_privacyidea_config(WEBAUTHNCONFIG.APP_ID, APP_ID)
-
-        self.user = User(login=USER_NAME, realm=self.realm1,
-                         resolver=self.resolvername1)
-
         self.token = init_token({
             'type': 'webauthn',
             'pin': '1234'
-        }, user=self.user)
+        })
 
         self.init_params = {
             WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
@@ -258,9 +193,11 @@ class WebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT,
             WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: DEFAULT_AUTHENTICATOR_ATTESTATION_FORM,
             WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT: DEFAULT_USER_VERIFICATION_REQUIREMENT,
-            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
+            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
         }
 
+        self.user = User(login=USER_NAME)
+
         self.challenge_options = {
             "user": self.user,
             WEBAUTHNACTION.ALLOWED_TRANSPORTS: ALLOWED_TRANSPORTS,
@@ -268,9 +205,15 @@ class WebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT
         }
 
-    def tearDown(self):
-        self.token.delete_token()
-        delete_policy("WebAuthn")
+    def test_00_users(self):
+        self.setUp_user_realms()
+
+        set_policy(name="WebAuthn",
+                   scope=SCOPE.ENROLL,
+                   action=WEBAUTHNACTION.RELYING_PARTY_NAME + "=" + RP_NAME + ","
+                         +WEBAUTHNACTION.RELYING_PARTY_ID   + "=" + RP_ID)
+        set_privacyidea_config(WEBAUTHNCONFIG.TRUST_ANCHOR_DIR, TRUST_ANCHOR_DIR)
+        set_privacyidea_config(WEBAUTHNCONFIG.APP_ID, APP_ID)
 
     def test_01_create_token(self):
         self.assertEqual(self.token.type, "webauthn")
@@ -280,8 +223,6 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertTrue(self.token.token.serial.startswith("WAN"))
         with self.assertRaises(ValueError):
             self.token.get_init_detail()
-        with self.assertRaises(ParameterError):
-            self.token.get_init_detail(self.init_params)
 
     def test_02_token_init(self):
         web_authn_register_request = self\
@@ -294,23 +235,28 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertIn('name', web_authn_register_request.get("relyingParty"))
         self.assertEqual(RP_ID, web_authn_register_request.get("relyingParty").get('id'))
         self.assertEqual(RP_NAME, web_authn_register_request.get("relyingParty").get('name'))
-        self.assertIn('alg', web_authn_register_request.get("pubKeyCredAlgorithms")[0],
-                      web_authn_register_request)
-        self.assertIn('type', web_authn_register_request.get("pubKeyCredAlgorithms")[0],
-                      web_authn_register_request)
-        self.assertEqual('public-key',
-                         web_authn_register_request.get("pubKeyCredAlgorithms")[0].get("type"),
-                         web_authn_register_request)
-        self.assertEqual(COSE_ALGORITHM.ES256,
-                         web_authn_register_request.get("pubKeyCredAlgorithms")[0].get("alg"),
-                         web_authn_register_request)
+        self.assertIn('alg', web_authn_register_request.get("preferredAlgorithm"))
+        self.assertIn('type', web_authn_register_request.get("preferredAlgorithm"))
+        self.assertEqual('public-key', web_authn_register_request.get("preferredAlgorithm").get("type"))
+        self.assertEqual(COSE_ALGORITHM.ES256, web_authn_register_request.get("preferredAlgorithm").get("alg"))
         self.assertEqual(USER_NAME, web_authn_register_request.get("name"))
 
         self.assertEqual(RP_ID, self.token.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_ID))
         self.assertEqual(RP_NAME, self.token.get_tokeninfo(WEBAUTHNINFO.RELYING_PARTY_NAME))
 
     def test_03_token_update(self):
-        self._setup_token()
+        with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
+            mock_nonce.return_value = webauthn_b64_decode(REGISTRATION_CHALLENGE)
+            self.token.get_init_detail(self.init_params, self.user)
+        self.token.update({
+            'type': 'webauthn',
+            'serial': self.token.token.serial,
+            'regdata': REGISTRATION_RESPONSE_TMPL['attObj'],
+            'clientdata': REGISTRATION_RESPONSE_TMPL['clientData'],
+            WEBAUTHNACTION.RELYING_PARTY_ID: RP_ID,
+            WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_LEVEL: ATTESTATION_LEVEL.NONE,
+            'HTTP_ORIGIN': ORIGIN
+        })
         web_authn_registration_response = self.token.get_init_detail().get("webAuthnRegisterResponse")
 
         self.assertTrue(web_authn_registration_response
@@ -334,31 +280,24 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.assertEqual(WebAuthnTokenClass.get_class_info('type'), "webauthn")
         self.assertTrue(self.token.token.serial.startswith("WAN"))
 
-        # we need to create an additional token for this to take effect
-        temp_token = init_token({
-            'type': 'webauthn',
-            'pin': '1234'
-        }, user=self.user)
-
         # No avoid double registration
         init_params = self.init_params
-        web_authn_register_request = temp_token \
+        web_authn_register_request = self \
+            .token \
             .get_init_detail(init_params, self.user) \
             .get("webAuthnRegisterRequest")
-        self.assertEqual(temp_token.token.serial, web_authn_register_request.get("serialNumber"))
+        self.assertEqual(self.token.token.serial, web_authn_register_request.get("serialNumber"))
         self.assertNotIn("excludeCredentials", web_authn_register_request)
 
-        self._setup_token()
-
         # Set avoid double registration
         init_params[WEBAUTHNACTION.AVOID_DOUBLE_REGISTRATION] = True
-        web_authn_register_request = temp_token \
+        web_authn_register_request = self \
+            .token \
             .get_init_detail(init_params, self.user) \
             .get("webAuthnRegisterRequest")
-        self.assertEqual(temp_token.token.serial, web_authn_register_request.get("serialNumber"))
+        self.assertEqual(self.token.token.serial, web_authn_register_request.get("serialNumber"))
         # Now the excludeCredentials is contained
         self.assertIn("excludeCredentials", web_authn_register_request)
-        temp_token.delete_token()
 
     def test_04_authentication(self):
         reply_dict = self._create_challenge()
@@ -386,27 +325,20 @@ class WebAuthnTokenTestCase(MyTestCase):
                                           })
         self.assertTrue(sign_count > 0)
 
-    @log_capture()
-    def test_06_missing_origin(self, capture):
+    def test_06_missing_origin(self):
+        self.challenge_options['nonce'] = webauthn_b64_decode(ASSERTION_CHALLENGE)
         self._create_challenge()
-        sign_count = self.token.check_otp(
-            otpval=None,
-            options={
-                "credentialid": CRED_ID,
-                "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-                "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-                "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-                "user": self.user,
-                "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-                "HTTP_ORIGIN": '',
-            })
+        sign_count = self.token.check_otp(otpval=None,
+                                          options={
+                                              "credentialid": CRED_ID,
+                                              "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
+                                              "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
+                                              "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
+                                              "user": self.user,
+                                              "challenge": hexlify_and_unicode(self.challenge_options['nonce']),
+                                              "HTTP_ORIGIN": '',
+                                          })
         self.assertTrue(sign_count == -1)
-        capture.check_present(
-            ('privacyidea.lib.tokens.webauthntoken',
-             'WARNING',
-             'Checking response for token {0!s} failed. HTTP Origin header '
-             'missing.'.format(self.token.get_serial()))
-        )
 
     def test_07_none_attestation(self):
         with patch('privacyidea.lib.tokens.webauthntoken.WebAuthnTokenClass._get_nonce') as mock_nonce:
@@ -433,7 +365,7 @@ class WebAuthnTokenTestCase(MyTestCase):
         self.user = User(login=NONE_ATTESTATION_USER_NAME)
         self.token.get_init_detail(self.init_params, self.user)
 
-        with self.assertRaises(EnrollmentError):
+        with self.assertRaises(RegistrationRejectedException):
             self.token.update({
                 'type': 'webauthn',
                 'serial': self.token.token.serial,
@@ -444,78 +376,10 @@ class WebAuthnTokenTestCase(MyTestCase):
                 'HTTP_ORIGIN': ORIGIN
             })
 
-    def test_09a_attestation_subject_allowed(self):
-        self._setup_token()
-        options = {
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.REQ: ['subject/.*Yubico.*/']
-        }
-        res = self.token.check_otp(otpval=None, options=options)
-        self.assertGreaterEqual(res, 0)
-
-    def test_09b_attestation_subject_not_allowed(self):
-        self._setup_token()
-        options = {
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.REQ: ['subject/.*Feitian.*/']
-        }
-        self.assertRaisesRegexp(PolicyError,
-                                "The WebAuthn token is not allowed to authenticate "
-                                "due to a policy restriction.",
-                                self.token.check_otp,
-                                **{'otpval': None,
-                                   'options': options})
-
-    def test_10a_aaguid_allowed(self):
-        self._setup_token()
-        # check token with a valid aaguid
-        res = self.token.check_otp(otpval=None, options={
-            "credentialid": CRED_ID,
-            "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-            "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-            "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-            "user": self.user,
-            "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-            "HTTP_ORIGIN": ORIGIN,
-            WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST: ['00000000000000000000000000000000']
-        })
-        self.assertGreaterEqual(res, 0)
-
-    def test_10b_aaguid_not_allowed(self):
-        self._setup_token()
-        # check token with an invalid aaguid
-        self.assertRaisesRegexp(PolicyError,
-                                "The WebAuthn token is not allowed to authenticate "
-                                "due to a policy restriction.",
-                                self.token.check_otp,
-                                **{'otpval': None,
-                                   'options': {
-                                       "credentialid": CRED_ID,
-                                       "authenticatordata": ASSERTION_RESPONSE_TMPL['authData'],
-                                       "clientdata": ASSERTION_RESPONSE_TMPL['clientData'],
-                                       "signaturedata": ASSERTION_RESPONSE_TMPL['signature'],
-                                       "user": self.user,
-                                       "challenge": hexlify_and_unicode(webauthn_b64_decode(ASSERTION_CHALLENGE)),
-                                       "HTTP_ORIGIN": ORIGIN,
-                                       WEBAUTHNACTION.AUTHENTICATOR_SELECTION_LIST: ['ffff0000000000000000000000000000']
-                                   }})
-
 
 class WebAuthnTestCase(unittest.TestCase):
     @staticmethod
-    def getWebAuthnCredential():
+    def getVerifiedRegistration():
         return WebAuthnRegistrationResponse(
             rp_id=RP_ID,
             origin=ORIGIN,
@@ -529,16 +393,16 @@ class WebAuthnTestCase(unittest.TestCase):
 
     @staticmethod
     def getAssertionResponse():
-        credential = WebAuthnTestCase.getWebAuthnCredential()
+        verified_registration = WebAuthnTestCase.getVerifiedRegistration()
         webauthn_user = WebAuthnUser(
             user_id=USER_ID,
             user_name=USER_NAME,
             user_display_name=USER_DISPLAY_NAME,
             icon_url=ICON_URL,
-            credential_id=credential.credential_id.decode(),
-            public_key=credential.public_key,
-            sign_count=credential.sign_count,
-            rp_id=credential.rp_id
+            credential_id=verified_registration.credential_id.decode(),
+            public_key=verified_registration.credential_public_key,
+            sign_count=verified_registration.sign_count,
+            rp_id=RP_ID
         )
 
         webauthn_assertion_response = WebAuthnAssertionResponse(
@@ -574,30 +438,9 @@ class WebAuthnTestCase(unittest.TestCase):
         self.assertTrue(CRED_KEY in registration_dict['pubKeyCredParams'])
 
     def test_01_validate_registration(self):
-        webauthn_credential = self.getWebAuthnCredential()
-        self.assertEqual(RP_ID, webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual(ORIGIN, webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_trusted_attestation, webauthn_credential)
-        self.assertEqual(str(webauthn_credential),
-                         '{0!r} ({1!s}, {2!s}, {3!s})'.format(CREDENTIAL_ID,
-                                                              RP_ID, ORIGIN, 0),
-                         webauthn_credential)
-
-    def test_01b_validate_untrusted_registration(self):
-        webauthn_credential = WebAuthnRegistrationResponse(
-            rp_id=RP_ID,
-            origin=ORIGIN,
-            registration_response=copy(REGISTRATION_RESPONSE_TMPL),
-            challenge=REGISTRATION_CHALLENGE,
-            attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.NONE],
-            uv_required=False,
-            expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS,
-        ).verify()
-        self.assertEqual(RP_ID, webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual(ORIGIN, webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertFalse(webauthn_credential.has_trusted_attestation, webauthn_credential)
+        verified_registration = self.getVerifiedRegistration()
+        self.assertEqual(CRED_ID, verified_registration.credential_id)
+        self.assertEqual(CRED_KEY, verified_registration.credential_public_key)
 
     def test_02_registration_invalid_user_verification(self):
         registration_response = WebAuthnRegistrationResponse(
@@ -611,17 +454,11 @@ class WebAuthnTestCase(unittest.TestCase):
             expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
         )
 
-        with self.assertRaisesRegexp(RegistrationRejectedException,
-                                     'Malformed request received.'):
+        with self.assertRaises(RegistrationRejectedException):
             registration_response.verify()
 
     def test_03_validate_assertion(self):
         webauthn_assertion_response = self.getAssertionResponse()
-        webauthn_user = webauthn_assertion_response.webauthn_user
-        self.assertEqual(str(webauthn_user),
-                         '{0!r} ({1!s}, {2!s}, {3!s})'.format(USER_ID, USER_NAME,
-                                                              USER_DISPLAY_NAME, 0),
-                         webauthn_user)
         webauthn_assertion_response.verify()
 
     def test_04_invalid_signature_fail_assertion(self):
@@ -666,76 +503,6 @@ class WebAuthnTestCase(unittest.TestCase):
     def test_07_webauthn_b64_decode(self):
         self.assertEqual(webauthn_b64_decode(URL_DECODE_TEST_STRING), URL_DECODE_EXPECTED_RESULT)
 
-    def test_08_registration_invalid_requirement_level(self):
-        with self.assertRaisesRegexp(ValueError,
-                                     'Illegal attestation_requirement_level.'):
-            WebAuthnRegistrationResponse(
-                rp_id=RP_ID,
-                origin=ORIGIN,
-                registration_response=copy(REGISTRATION_RESPONSE_TMPL),
-                challenge=REGISTRATION_CHALLENGE,
-                attestation_requirement_level={'unknown level': False},
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                uv_required=True,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            )
-
-    def test_09_registration_self_Attestation(self):
-        webauthn_credential = WebAuthnRegistrationResponse(
-            rp_id='localhost',
-            origin='http://localhost:3000',
-            registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_TMPL),
-            challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-            attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-            trust_anchor_dir=TRUST_ANCHOR_DIR,
-            expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-        ).verify()
-        self.assertEqual('localhost', webauthn_credential.rp_id, webauthn_credential)
-        self.assertEqual('http://localhost:3000', webauthn_credential.origin, webauthn_credential)
-        self.assertTrue(webauthn_credential.has_signed_attestation, webauthn_credential)
-        self.assertFalse(webauthn_credential.has_trusted_attestation, webauthn_credential)
-
-    def test_09b_registration_self_Attestation_bad_cose_alg(self):
-        self.assertRaises(
-            RegistrationRejectedException,
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_BAD_COSE_ALG),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
-    def test_09c_registration_self_Attestation_alg_mismatch(self):
-        self.assertRaisesRegexp(
-            RegistrationRejectedException,
-            'does not match algorithm from attestation statement',
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_ALG_MISMATCH),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
-    def test_09d_registration_self_Attestation_broken_signature(self):
-        self.assertRaisesRegexp(
-            RegistrationRejectedException,
-            'Invalid signature received.',
-            WebAuthnRegistrationResponse(
-                rp_id='localhost',
-                origin='http://localhost:3000',
-                registration_response=copy(SELF_ATTESTATION_REGISTRATION_RESPONSE_BROKEN_SIG),
-                challenge='AXkXWXPP3gLx8OLlpkJ3aRRhFWntnSENggnjDpBql1ngKol7xWwevUYvrpBDP3LEvdr2EOStOFpGGxnMvXk-Vw',
-                attestation_requirement_level=ATTESTATION_REQUIREMENT_LEVEL[ATTESTATION_LEVEL.UNTRUSTED],
-                trust_anchor_dir=TRUST_ANCHOR_DIR,
-                expected_registration_client_extensions=EXPECTED_REGISTRATION_CLIENT_EXTENSIONS
-            ).verify)
-
 
 class MultipleWebAuthnTokenTestCase(MyTestCase):
     rp_name = 'pitest'
@@ -806,7 +573,7 @@ class MultipleWebAuthnTokenTestCase(MyTestCase):
             WEBAUTHNACTION.TIMEOUT: TIMEOUT,
             WEBAUTHNACTION.AUTHENTICATOR_ATTESTATION_FORM: DEFAULT_AUTHENTICATOR_ATTESTATION_FORM,
             WEBAUTHNACTION.USER_VERIFICATION_REQUIREMENT: DEFAULT_USER_VERIFICATION_REQUIREMENT,
-            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHMS: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
+            WEBAUTHNACTION.PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE: PUBLIC_KEY_CREDENTIAL_ALGORITHM_PREFERENCE
         }
     auth_options = {
         WEBAUTHNACTION.ALLOWED_TRANSPORTS: ALLOWED_TRANSPORTS,