Repository: privacyidea/privacyidea

Issue:
Token type API endpoints should only process tokens of their respective types
The public ``/ttype/<ttype>`` API endpoints should only process tokens of type ``ttype``.

Example:
```
$ http POST http://localhost:5000/ttype/tiqr action=metadata session='' serial=OATH000201D6 secret=''  
HTTP/1.0 200 OK
Cache-Control: no-cache
Content-Length: 987
Content-Type: application/json
Date: Mon, 25 Mar 2019 12:18:40 GMT
Server: Werkzeug/0.14.1 Python/2.7.15

{
    "identity": {
        "displayName": "RealtimeKit ",
        "identifier": "rtkit_defrealm"
    },
    "service": {
        "authenticationUrl": "None",
        "displayName": "privacyIDEA",
        "enrollmentUrl": "None?action=enrollment&session=&serial=OATH000201D6",
        "identifier": "org.privacyidea",
        "infoUrl": "https://www.privacyidea.org",
        "logoUrl": null,
        "ocraSuite": "OCRA-1:HOTP-SHA1-6:QN10"
    },
    "signature": "..."
}
```

even though ``OATH000201D6`` is a hotp token.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/tokens/tiqrtoken.py b/privacyidea/lib/tokens/tiqrtoken.py
--- a/privacyidea/lib/tokens/tiqrtoken.py
+++ b/privacyidea/lib/tokens/tiqrtoken.py
@@ -85,7 +85,7 @@
 from privacyidea.lib.crypto import generate_otpkey
 from privacyidea.lib.utils import create_img
 import logging
-from privacyidea.lib.token import get_tokens
+from privacyidea.lib.token import get_one_token
 from privacyidea.lib.error import ParameterError
 from privacyidea.models import Challenge
 from privacyidea.lib.user import get_user_from_param
@@ -223,8 +223,8 @@ def get_init_detail(self, params=None, user=None):
 
         return response_detail
 
-    @staticmethod
-    def api_endpoint(request, g):
+    @classmethod
+    def api_endpoint(cls, request, g):
         """
         This provides a function to be plugged into the API endpoint
         /ttype/<tokentype> which is defined in api/ttype.py
@@ -246,10 +246,8 @@ def api_endpoint(request, g):
             serial = getParam(params, "serial", required)
             # The user identifier is displayed in the App
             # We need to set the user ID
-            tokens = get_tokens(serial=serial)
-            if not tokens:  # pragma: no cover
-                raise ParameterError("No token with serial {0!s}".format(serial))
-            user_identifier, user_displayname = tokens[0].get_user_displayname()
+            token = get_one_token(serial=serial, tokentype="tiqr")
+            user_identifier, user_displayname = token.get_user_displayname()
 
             service_identifier = get_from_config("tiqr.serviceIdentifier") or\
                                  "org.privacyidea"
@@ -300,16 +298,16 @@ def api_endpoint(request, g):
             # The secret needs to be stored in the token object.
             # We take the token "serial" and check, if it contains the "session"
             # in the tokeninfo.
-            enroll_tokens = get_tokens(serial=serial)
-            if len(enroll_tokens) == 1:
-                if enroll_tokens[0].get_tokeninfo("session") == session:
-                    # save the secret
-                    enroll_tokens[0].set_otpkey(secret)
-                    # delete the session
-                    enroll_tokens[0].del_tokeninfo("session")
-                    res = "OK"
-                else:
-                    raise ParameterError("Invalid Session")
+            enroll_token = get_one_token(serial=serial, tokentype="tiqr")
+            tokeninfo_session = enroll_token.get_tokeninfo("session")
+            if tokeninfo_session and tokeninfo_session == session:
+                # save the secret
+                enroll_token.set_otpkey(secret)
+                # delete the session
+                enroll_token.del_tokeninfo("session")
+                res = "OK"
+            else:
+                raise ParameterError("Invalid Session")
 
             return "plain", res
         elif action == API_ACTIONS.AUTHENTICATION:
@@ -328,16 +326,15 @@ def api_endpoint(request, g):
                 # Challenge is still valid (time has not passed) and no
                     # correct response was given.
                     serial = challenges[0].serial
-                    tokens = get_tokens(serial=serial)
-                    if len(tokens) == 1:
-                        # We found exactly the one token
-                        res = "INVALID_RESPONSE"
-                        r = tokens[0].verify_response(
-                            challenge=challenges[0].challenge, passw=passw)
-                        if r > 0:
-                            res = "OK"
-                            # Mark the challenge as answered successfully.
-                            challenges[0].set_otp_status(True)
+                    token = get_one_token(serial=serial, tokentype="tiqr")
+                    # We found exactly the one token
+                    res = "INVALID_RESPONSE"
+                    r = token.verify_response(
+                        challenge=challenges[0].challenge, passw=passw)
+                    if r > 0:
+                        res = "OK"
+                        # Mark the challenge as answered successfully.
+                        challenges[0].set_otp_status(True)
 
             cleanup_challenges()
 
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
@@ -40,7 +40,7 @@
                                         parse_registration_data, url_encode,
                                         parse_response_data, check_response,
                                         x509name_to_string)
-from privacyidea.lib.error import ValidateError, PolicyError
+from privacyidea.lib.error import ValidateError, PolicyError, ParameterError
 from privacyidea.lib.policy import (SCOPE, GROUP, ACTION,
                                     get_action_values_from_options)
 from privacyidea.lib.utils import (is_true, hexlify_and_unicode,
@@ -532,8 +532,8 @@ def check_otp(self, otpval, counter=None, window=None, options=None):
 
         return ret
 
-    @staticmethod
-    def api_endpoint(request, g):
+    @classmethod
+    def api_endpoint(cls, request, g):
         """
         This provides a function to be plugged into the API endpoint
         /ttype/u2f
@@ -544,7 +544,10 @@ def api_endpoint(request, g):
         :param g: The Flask global object g
         :return: Flask Response or text
         """
-        app_id = get_from_config("u2f.appId").strip("/")
+        configured_app_id = get_from_config("u2f.appId")
+        if configured_app_id is None:
+            raise ParameterError("u2f is not configured")
+        app_id = configured_app_id.strip("/")
 
         # Read the facets from the policies
         pol_facets = g.policy_object.get_action_values(U2FACTION.FACETS,
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
@@ -432,7 +432,7 @@ def check_yubikey_pass(passw):
                 serialnum = "UBAM" + modhex_decode(prefix)
                 for i in range(1, 3):
                     s = "{0!s}_{1!s}".format(serialnum, i)
-                    toks = get_tokens(serial=s)
+                    toks = get_tokens(serial=s, tokentype='yubikey')
                     token_list.extend(toks)
             except TypeError as exx:  # pragma: no cover
                 log.error("Failed to convert serialnumber: {0!r}".format(exx))


Test patch:
diff --git a/tests/test_api_u2f.py b/tests/test_api_u2f.py
--- a/tests/test_api_u2f.py
+++ b/tests/test_api_u2f.py
@@ -28,6 +28,11 @@ class APIU2fTestCase(MyApiTestCase):
 
     def test_000_setup_realms(self):
         self.setUp_user_realms()
+        # U2F is not configured yet
+        with self.app.test_request_context('/ttype/u2f',
+                                           method='GET'):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 400)
 
         set_privacyidea_config("u2f.appId", "http://localhost:5000")