Repository: privacyidea/privacyidea

Issue:
Feature request: allow image in the otpauth url
Some 2FA mobile apps such as FreeOTP (https://freeotp.github.io) support reading an optional image URL from the oathurl and then displaying it next to the software token. It would be nice if PrivacyIDEA could provide a way for easily configuring an image that will be included in the oath URL.

Here is an example oath URL that includes an image:
otpauth://hotp/FreeOTP:Fr?secret=fewx46epiyeqlfapjwlleryqi6bdxj36bhlelhjgk6r725ps7qsqbgon&algorithm=SHA1&digits=6&period=30&counter=0&image=https%3A%2F%2Ffreeotp.github.io%2Fimg%2Ffreeotp.svg


Discussion:
OK, it is an ``image`` parameter. Does it have to be an SVG?

The downside of FreeOTP is, that it still does not support the 2step enrollment of privacyIDEA! ;-)

I think we can easily add this. 
See here:
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/policy.py#L1520

~~~~python
            ACTION.APPIMAGEURL: {
                'type': 'str',
                'desc': _("This is the URL to the token image for smartphone apps like FreeOTP."),
                'group': GROUP.TOKEN
            },
~~~~

Resolving patch:
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -498,6 +498,16 @@ def init_tokenlabel(request=None, action=None):
     if len(issuer_pols) == 1:
         request.all_data["tokenissuer"] = issuer_pols[0]
 
+    imageurl_pols = policy_object.get_action_values(action=ACTION.APPIMAGEURL,
+                                                    scope=SCOPE.ENROLL,
+                                                    user=user_object.login,
+                                                    realm=user_object.realm,
+                                                    client=g.client_ip,
+                                                    unique=True,
+                                                    allow_white_space_in_action=True)
+    if len(imageurl_pols) == 1:
+        request.all_data["appimageurl"] = imageurl_pols[0]
+
     return True
 
 
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 #
+#  2018-09-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
+#             Add App Image URL
 #  2018-01-15 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #             Add tokeninfo field policy
 #             Add add_resolver_in_result
@@ -198,6 +200,7 @@ class SCOPE(object):
 class ACTION(object):
     __doc__ = """This is the list of usual actions."""
     ASSIGN = "assign"
+    APPIMAGEURL = "appimageurl"
     AUDIT = "auditlog"
     AUDIT_AGE = "auditlog_age"
     AUDIT_DOWNLOAD = "auditlog_download"
@@ -1523,6 +1526,12 @@ def get_static_policy_definitions(scope=None):
                           "Authenticators."),
                 'group': GROUP.TOKEN
             },
+            ACTION.APPIMAGEURL: {
+                'type': 'str',
+                'desc': _("This is the URL to the token image for smartphone apps "
+                          "like FreeOTP."),
+                'group': GROUP.TOKEN
+            },
             ACTION.AUTOASSIGN: {
                 'type': 'str',
                 'value': [AUTOASSIGNVALUE.NONE, AUTOASSIGNVALUE.USERSTORE],
diff --git a/privacyidea/lib/tokens/hotptoken.py b/privacyidea/lib/tokens/hotptoken.py
--- a/privacyidea/lib/tokens/hotptoken.py
+++ b/privacyidea/lib/tokens/hotptoken.py
@@ -217,6 +217,9 @@ def get_init_detail(self, params=None, user=None):
             twostep_parameters = self._get_twostep_parameters()
             extra_data.update(twostep_parameters)
             response_detail.update(twostep_parameters)
+        imageurl = params.get("appimageurl")
+        if imageurl:
+            extra_data.update({"image": imageurl})
         if otpkey:
             tok_type = self.type.lower()
             if user is not None:


Test patch:
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -1452,4 +1452,27 @@ def test_27_fail_to_assign_empty_serial(self):
             self.assertTrue(res.status_code == 400, res)
             result = json.loads(res.data).get("result")
             self.assertEqual(result.get("status"), False)
-            self.assertEqual(result.get("error").get("code"), 905)
\ No newline at end of file
+            self.assertEqual(result.get("error").get("code"), 905)
+
+    def test_28_enroll_app_with_image_url(self):
+        set_policy("imgurl", scope=SCOPE.ENROLL,
+                   action="{0!s}=https://example.com/img.png".format(ACTION.APPIMAGEURL))
+        with self.app.test_request_context('/token/init',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "genkey": "1",
+                                                 "realm": self.realm1,
+                                                 "serial": "goog1",
+                                                 "pin": "test"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data).get("result")
+            detail = json.loads(res.data).get("detail")
+            self.assertTrue(result.get("status"))
+            self.assertTrue(result.get("value"))
+            self.assertTrue(u'image=https%3A//example.com/img.png' in detail.get("googleurl").get("value"),
+                            detail.get("googleurl"))
+
+        remove_token("goog1")
+        delete_policy("imgurl")
\ No newline at end of file