Repository: privacyidea/privacyidea

Issue:
usability adding yubikey tokens: automatically remove spaces?
I'm adding a new yubikey token and use ykpersonalize and privacyidea's webui. In our documentation https://privacyidea.readthedocs.io/en/latest/configuration/tokens/yubikey.html we have:
(Remove possible white spaces!)
That's something a computer can do very easily - we should allow to use cut&paste without manual editing to avoid possible errors.
* privacyIDEA Version: 3.0.1

* Installation method: ubuntu packages



Discussion:
I agree, removing whitespace should (hopefully!) be somewhat easy to implement :-) In the case of the yubikey tokenclass, I would probably implement it in the ``update`` method right before passing the otpkey down to ``TokenClass.update``?

https://github.com/privacyidea/privacyidea/blob/b7fe9ae20cdba41d8c4de1259f204fe4c3ae839d/privacyidea/lib/tokens/yubikeytoken.py#L459

And I guess we would also need to adjust the otpkey pattern here:

https://github.com/privacyidea/privacyidea/blob/b7fe9ae20cdba41d8c4de1259f204fe4c3ae839d/privacyidea/static/components/token/views/token.enroll.yubikey.html#L41


Resolving patch:
diff --git a/privacyidea/lib/tokens/yubikeytoken.py b/privacyidea/lib/tokens/yubikeytoken.py
--- a/privacyidea/lib/tokens/yubikeytoken.py
+++ b/privacyidea/lib/tokens/yubikeytoken.py
@@ -456,5 +456,10 @@ def check_yubikey_pass(passw):
 
     @log_with(log)
     def update(self, param, reset_failcount=True):
-        TokenClass.update(self, param, reset_failcount)
+        update_params = param.copy()
+        # As the secret is usually copy-pasted from the Yubikey personalization GUI,
+        # which separates hexlified bytes by spaces, we remove all spaces from the OTP key.
+        if "otpkey" in update_params:
+            update_params["otpkey"] = update_params["otpkey"].replace(" ", "")
+        TokenClass.update(self, update_params, reset_failcount)
         self.add_tokeninfo("tokenkind", TOKENKIND.HARDWARE)
\ No newline at end of file


Test patch:
diff --git a/tests/test_lib_tokens_yubikey.py b/tests/test_lib_tokens_yubikey.py
--- a/tests/test_lib_tokens_yubikey.py
+++ b/tests/test_lib_tokens_yubikey.py
@@ -199,6 +199,43 @@ def test_10_api_endpoint(self):
         self.assertTrue("status=OK" in result, result)
         self.assertTrue("nonce={0!s}".format(nonce) in result, result)
 
+    def test_11_strip_whitespace(self):
+        fixed = "ebedeeefegeheiej"
+        # The backend automatically strips whitespace from the OTP key
+        otpkey = "cc 17 a4 d7 7e ae d9 6e 9d 14 b5 c8 7a 02 e7 18"
+        uid = "000000000000"
+        otps = ["ebedeeefegeheiejtjtrutblehenfjljrirgdihrfuetljtt",
+                "ebedeeefegeheiejlekvlrlkrcluvctenlnnjfknrhgtjned",
+                "ebedeeefegeheiejktudedbktcnbuntrhdueikggtrugckij",
+                "ebedeeefegeheiejjvjncbnffdrvjcvrbgdfufjgndfetieu",
+                "ebedeeefegeheiejdruibhvlvktcgfjiruhltketifnitbuk"
+        ]
+
+        token = init_token({"type": "yubikey",
+                            "otpkey": otpkey,
+                            "otplen": len(otps[0]),
+                            "yubikey.prefix": fixed,
+                            "serial": "UBAM12345678_1"})
+
+        builder = EnvironBuilder(method='GET',
+                                 headers={})
+        env = builder.get_environ()
+        # Set the remote address so that we can filter for it
+        env["REMOTE_ADDR"] = "10.0.0.1"
+        g.client_ip = env["REMOTE_ADDR"]
+        req = Request(env)
+        nonce = "random nonce"
+        apiid = "hallo"
+        apikey = "1YMEbMZijD3DzL21UfKGnOOI13c="
+        set_privacyidea_config("yubikey.apiid.{0!s}".format(apiid), apikey)
+        req.all_data = {'id': apiid,
+                        "otp": otps[0],
+                        "nonce": nonce}
+        text_type, result = YubikeyTokenClass.api_endpoint(req, g)
+        self.assertEqual(text_type, "plain")
+        self.assertTrue("status=OK" in result, result)
+        self.assertTrue("nonce={0!s}".format(nonce) in result, result)
+
     def test_98_wrong_tokenid(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = YubikeyTokenClass(db_token)