Repository: privacyidea/privacyidea

Issue:
Convert image url to data:image
The u2ftoken and webauthn token return images of certain tokens, that are supposed to be displayed during challenge response authentication.

See here:
https://github.com/privacyidea/privacyidea/blob/743b68498d0381b41f6297b39c21627e9bce9379/privacyidea/lib/tokens/u2ftoken.py#L485

and here:
https://github.com/privacyidea/privacyidea/blob/4487178f04609a03a45a510f1c8296bdcf01ed79/privacyidea/lib/tokens/webauthntoken.py#L1182

However, these are links to files on the privacyIDEA server and can not be used by plugins.

What we need to do is load this PNG and convert it do a `data:image`. This way the plugins will be able to display these images.

There is no need to change anything in the privacyIDEA webui:
https://github.com/privacyidea/privacyidea/blob/fd9c5035806585e2c312bbdd023db7cd554971bf/privacyidea/static/components/login/controllers/loginControllers.js#L276

Putting @lukasmatusiewicz in CC for notifications.

* [x] implement utility function
* [x] add code to webauthn and u2f
* [x] test webui


Discussion:
Rather simple base64 encoding.
See: https://gist.github.com/jsocol/1089733


Resolving patch:
diff --git a/privacyidea/lib/tokens/u2ftoken.py b/privacyidea/lib/tokens/u2ftoken.py
--- a/privacyidea/lib/tokens/u2ftoken.py
+++ b/privacyidea/lib/tokens/u2ftoken.py
@@ -45,7 +45,7 @@
 from privacyidea.lib.policy import SCOPE, GROUP, ACTION, get_action_values_from_options
 from privacyidea.lib.policy import Match
 from privacyidea.lib.challenge import get_challenges
-from privacyidea.lib.utils import is_true, hexlify_and_unicode, to_unicode
+from privacyidea.lib.utils import is_true, hexlify_and_unicode, to_unicode, convert_imagefile_to_dataimage
 import binascii
 import json
 
@@ -188,10 +188,11 @@
 #
 # The solokeys image is copyright (C) 2020 Solokeys. License: CC-BY-SA 4.0
 #
-IMAGES = {"yubico": "static/img/FIDO-U2F-Security-Key-444x444.png",
-          "plug-up": "static/img/plugup.jpg",
-          "u2fzero.com": "static/img/u2fzero.png",
-          "solokeys": "static/img/solokeys.png"}
+# The image is a relative file system path.
+IMAGES = {"yubico": "privacyidea/static/img/FIDO-U2F-Security-Key-444x444.png",
+          "plug-up": "privacyidea/static/img/plugup.jpg",
+          "u2fzero.com": "privacyidea/static/img/u2fzero.png",
+          "solokeys": "privacyidea/static/img/solokeys.png"}
 
 U2F_Version = "U2F_V2"
 
@@ -483,10 +484,11 @@ def create_challenge(self, transactionid=None, options=None):
                             "keyHandle": key_handle_url}
 
         image_url = IMAGES.get(self.token.description.lower().split()[0], "")
+        dataimage = convert_imagefile_to_dataimage(image_url) if image_url else ""
         reply_dict = {"attributes": {"u2fSignRequest": u2f_sign_request,
                                      "hideResponseInput": self.client_mode != CLIENTMODE.INTERACTIVE,
-                                     "img": image_url},
-                      "image": image_url}
+                                     "img": dataimage},
+                      "image": dataimage}
 
         return True, message, db_challenge.transaction_id, reply_dict
 
diff --git a/privacyidea/lib/tokens/webauthntoken.py b/privacyidea/lib/tokens/webauthntoken.py
--- a/privacyidea/lib/tokens/webauthntoken.py
+++ b/privacyidea/lib/tokens/webauthntoken.py
@@ -45,7 +45,7 @@
 from privacyidea.lib import _
 from privacyidea.lib.policy import SCOPE, GROUP, ACTION
 from privacyidea.lib.user import User
-from privacyidea.lib.utils import hexlify_and_unicode, is_true
+from privacyidea.lib.utils import hexlify_and_unicode, is_true, convert_imagefile_to_dataimage
 
 __doc__ = """
 WebAuthn  is the Web Authentication API specified by the FIDO Alliance.
@@ -1176,10 +1176,11 @@ def create_challenge(self, transactionid=None, options=None):
                              required)
         ).assertion_dict
 
+        dataimage = convert_imagefile_to_dataimage(user.icon_url) if user.icon_url else ""
         reply_dict = {"attributes": {"webAuthnSignRequest": public_key_credential_request_options,
                                      "hideResponseInput": self.client_mode != CLIENTMODE.INTERACTIVE,
-                                     "img": user.icon_url},
-                      "image": user.icon_url}
+                                     "img": dataimage},
+                      "image": dataimage}
 
         return True, message, db_challenge.transaction_id, reply_dict
 
diff --git a/privacyidea/lib/utils/__init__.py b/privacyidea/lib/utils/__init__.py
--- a/privacyidea/lib/utils/__init__.py
+++ b/privacyidea/lib/utils/__init__.py
@@ -26,6 +26,8 @@
 
 This module is tested in tests/test_lib_utils.py
 """
+import os
+
 import six
 import logging
 from importlib import import_module
@@ -46,6 +48,7 @@
 import time
 import html
 import segno
+import mimetypes
 
 from privacyidea.lib.error import ParameterError, ResourceNotFoundError, PolicyError
 
@@ -1489,3 +1492,25 @@ def replace_function_event_handler(text, token_serial=None, tokenowner=None, log
     except(ValueError, KeyError) as err:
         log.warning("Unable to replace placeholder: ({0!s})! Please check the webhooks data option.".format(err))
         return text
+
+
+def convert_imagefile_to_dataimage(imagepath):
+    """
+    This helper reads an image file and converts it to a dataimage string,
+    that can be directly used in HTML pages.
+
+    :param imagepath:
+    :return: A dataimage as string
+    """
+    try:
+        mime, _ = mimetypes.guess_type(imagepath)
+        if not mime:
+            log.warning("Unknown file type in file {0!s}.".format(imagepath))
+            return ""
+        with open(imagepath, "rb") as f:
+            data = f.read()
+            data64 = base64.b64encode(data)
+        return "data:{0!s};base64,{1!s}".format(mime, to_unicode(data64))
+    except FileNotFoundError:
+        log.warning("The file {0!s} could not be found.".format(imagepath))
+        return ""


Test patch:
diff --git a/tests/test_lib_utils.py b/tests/test_lib_utils.py
--- a/tests/test_lib_utils.py
+++ b/tests/test_lib_utils.py
@@ -24,7 +24,7 @@
                                    modhex_decode, checksum, urlsafe_b64encode_and_unicode,
                                    check_ip_in_policy, split_pin_pass, create_tag_dict,
                                    check_serial_valid, determine_logged_in_userparams,
-                                   to_list, parse_string_to_dict)
+                                   to_list, parse_string_to_dict, convert_imagefile_to_dataimage)
 from privacyidea.lib.crypto import generate_password
 from datetime import timedelta, datetime
 from netaddr import IPAddress, IPNetwork, AddrFormatError
@@ -1012,3 +1012,19 @@ def test_35_parse_string_to_dict(self):
         self.assertEqual(d.get("key1"), ["v1", "v2", "v3"])
         self.assertEqual(d.get("key2"), [])
         self.assertEqual(d.get("key3"), ["v5"])
+
+    def test_36_imagefile_to_dataimage(self):
+        file = "./tests/testdata/FIDO-U2F-Security-Key-444x444.png"
+        dataimage = convert_imagefile_to_dataimage(file)
+        self.assertTrue(dataimage.startswith("data:image/png;base64,iVBOR"))
+
+        # File not found returns an empty datatime string
+        file = "./tests/testdata/FIDO-U2F-Security-Key-444x444.XXX"
+        dataimage = convert_imagefile_to_dataimage(file)
+        self.assertEqual("", dataimage)
+
+        # Try to read a grazy file with an unknown mime-type
+        file = "./tests/testdata/logging.yml"
+        dataimage = convert_imagefile_to_dataimage(file)
+        self.assertEqual("", dataimage)
+
diff --git a/tests/testdata/FIDO-U2F-Security-Key-444x444.png b/tests/testdata/FIDO-U2F-Security-Key-444x444.png
new file mode 100644
Binary files /dev/null and b/tests/testdata/FIDO-U2F-Security-Key-444x444.png differ