def test_01_challenge(self):

        set_policy("chalresp", scope=SCOPE.AUTHZ,
                   action="{0!s}=hotp".format(ACTION.CHALLENGERESPONSE))
        token = init_token({"genkey": 1, "serial": "CHAL1", "pin": "pin"})
        from privacyidea.lib.token import check_serial_pass
        r = check_serial_pass(token.token.serial, "pin")
        # The OTP PIN is correct
        self.assertEqual(r[0], False)
        self.assertEqual(r[1].get("message"), "please enter otp: ")
        transaction_id = r[1].get("transaction_id")
        chals = get_challenges()
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # get challenge for this serial
        chals = get_challenges(serial="CHAL1")
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # get challenge for another seial
        chals = get_challenges(serial="CHAL2")
        self.assertEqual(len(chals), 0)

        delete_policy("chalresp")
Exemple #2
0
    def test_01_challenge(self):

        set_policy("chalresp",
                   scope=SCOPE.AUTHZ,
                   action="{0!s}=hotp".format(ACTION.CHALLENGERESPONSE))
        token = init_token({"genkey": 1, "serial": "CHAL1", "pin": "pin"})
        from privacyidea.lib.token import check_serial_pass
        r = check_serial_pass(token.token.serial, "pin")
        # The OTP PIN is correct
        self.assertEqual(r[0], False)
        self.assertEqual(r[1].get("message"), _("please enter otp: "))
        transaction_id = r[1].get("transaction_id")
        chals = get_challenges()
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # get challenge for this serial
        chals = get_challenges(serial="CHAL1")
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # get challenge for another seial
        chals = get_challenges(serial="CHAL2")
        self.assertEqual(len(chals), 0)

        delete_policy("chalresp")
 def mark_challenge_as_accepted(self):
     # We simply mark all challenges as successfully answered!
     with self.app.test_request_context():
         challenges = get_challenges()
         for chal in challenges:
             chal.set_otp_status(True)
             chal.save()
Exemple #4
0
    def test_01_multiple_token(self):
        set_policy("otppin",
                   scope=SCOPE.AUTH,
                   action="{0!s}=none".format(ACTION.OTPPIN))
        res, reply = check_user_pass(self.user, '')
        self.assertFalse(res)
        self.assertIn('transaction_id', reply, reply)
        tid = reply['transaction_id']
        self.assertIn('multi_challenge', reply, reply)
        self.assertEqual(len(reply['multi_challenge']), 2,
                         reply['multi_challenge'])
        self.assertIn('messages', reply, reply)
        self.assertEqual(len(reply['messages']), 2, reply['messages'])
        # check that the serials of the challenges are different
        chal1 = reply['multi_challenge'][0]
        chal2 = reply['multi_challenge'][1]
        self.assertNotEqual(chal1['serial'], chal2['serial'],
                            reply['multi_challenge'])
        self.assertEqual(chal1['transaction_id'], chal2['transaction_id'],
                         reply['multi_challenge'])
        # Now make sure that the requests contain the same challenge
        self.assertEqual(chal1['attributes']['u2fSignRequest']['challenge'],
                         chal2['attributes']['u2fSignRequest']['challenge'],
                         reply['multi_challenge'])
        # check that we have two challenges in the db with the same challenge
        chals = get_challenges(transaction_id=tid)
        self.assertEqual(len(chals), 2, chals)
        self.assertEqual(chals[0].challenge, chals[1].challenge, chals)

        delete_policy('otppin')
Exemple #5
0
    def test_13_privacyidea_challenge_response(self):
        # This tests the challenge response with the privacyIDEA PIN.
        # First an authentication request with only the local PIN of the
        # radius token is sent.
        r = add_radius(identifier="myserver", server="1.2.3.4",
                       secret="testing123", dictionary=DICT_FILE)
        self.assertTrue(r > 0)
        token = init_token({"type": "radius",
                            "pin": "local",
                            "radius.identifier": "myserver",
                            "radius.local_checkpin": True,
                            "radius.user": u"nönäscii"})

        r = token.is_challenge_request("local")
        self.assertTrue(r)

        # create challenge of privacyidea
        r, message, transaction_id, _attr = token.create_challenge()
        self.assertTrue(r)
        self.assertEqual("Enter your RADIUS tokencode:", message)

        # check, if there is a challenge in the DB
        chals = get_challenges(token.token.serial)
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # check if this is a response to a previously sent challenge
        r = token.is_challenge_response("radiuscode", options={"transaction_id": transaction_id})
        self.assertTrue(r)

        # Now check, if the answer for the challenge is correct
        radiusmock.setdata(response=radiusmock.AccessAccept)
        r = token.check_challenge_response(passw="radiuscode",
                                           options={"transaction_id": transaction_id})
        self.assertTrue(r)
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if there is a matching question for the given
        passw and also verifies if the answer is correct.

        It then returns the the otp_counter = 1

        :param user: the requesting user
        :type user: User object
        :param passw: the password - in fact it is the answer to the question
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return otp_counter. If -1, challenge does not match
        :rtype: int
        """
        if options is None:
            options = {}
        otp_counter = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id') or options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    state = binascii.unhexlify(challengeobject.data)

                    # challenge is still valid
                    radius_response = self._check_radius(passw,
                                                         options=options,
                                                         radius_state=state)
                    if radius_response == AccessAccept:
                        # We found the matching challenge,
                        # and the RADIUS server returned AccessAccept
                        challengeobject.delete()
                        otp_counter = 1
                        break
                    elif radius_response == AccessChallenge:
                        # The response was valid but triggered a new challenge
                        # Note: The second challenge currently does not work correctly
                        # see https://github.com/privacyidea/privacyidea/issues/1792
                        challengeobject.delete()
                        _, _, transaction_id, _ = self.create_challenge(
                            options=options)
                        options["transaction_id"] = transaction_id
                        otp_counter = -1
                        break
                    else:
                        otp_counter = -1
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return otp_counter
    def has_further_challenge(self, options=None):
        """
        Check if we should do multi challenge at all and
        then if there are further positions to query.

        :param options: Options dict
        :return: True, if further challenge is required.
        """
        if self.get_tokeninfo("multichallenge"):
            transaction_id = options.get('transaction_id')
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            position_count = int(
                get_action_values_from_options(
                    SCOPE.AUTH, "{0!s}_{1!s}".format(
                        self.get_class_type(), PIIXACTION.COUNT), options)
                or DEFAULT_POSITION_COUNT)
            if len(challengeobject_list) == 1:
                session = int(challengeobject_list[0].session or "0") + 1
                options["session"] = u"{0!s}".format(session)
                if session < position_count:
                    return True

        return False
Exemple #8
0
def poll_transaction(transaction_id=None):
    """
    Given a mandatory transaction ID, check if any non-expired challenge for this transaction ID
    has been answered. In this case, return true. If this is not the case, return false.
    This endpoint also returns false if no challenge with the given transaction ID exists.

    This is mostly useful for out-of-band tokens that should poll this endpoint
    to determine when to send an authentication request to ``/validate/check``.

    :jsonparam transaction_id: a transaction ID
    """
    if transaction_id is None:
        transaction_id = getParam(request.all_data, "transaction_id", required)
    # Fetch a list of non-exired challenges with the given transaction ID
    # and determine whether it contains at least one non-expired answered challenge.
    matching_challenges = [
        challenge
        for challenge in get_challenges(transaction_id=transaction_id)
        if challenge.is_valid()
    ]
    answered_challenges = extract_answered_challenges(matching_challenges)

    if answered_challenges:
        result = True
        log_challenges = answered_challenges
    else:
        result = False
        log_challenges = matching_challenges

    # We now determine the information that should be written to the audit log:
    # * If there are no answered valid challenges, we log all token serials of challenges matching
    #   the transaction ID and the corresponding token owner
    # * If there are any answered valid challenges, we log their token serials and the corresponding user
    if log_challenges:
        g.audit_object.log({
            "serial":
            ",".join(challenge.serial for challenge in log_challenges),
        })
        # The token owner should be the same for all matching transactions
        user = get_one_token(serial=log_challenges[0].serial).user
        if user:
            g.audit_object.log({
                "user": user.login,
                "resolver": user.resolver,
                "realm": user.realm,
            })

    # In any case, we log the transaction ID
    g.audit_object.log({
        "info": u"transaction_id: {}".format(transaction_id),
        "success": result
    })

    return send_result(result)
Exemple #9
0
    def test_14_simple_challenge_response_in_radius_server(self):
        # In this case we test a simple challenge response in
        # the radius server. The PIN is checked locally.
        # A AccessRequest is sent to the RADIUS server, the RADIUS server
        # answers with an AccessChallenge, which creates a transaction id
        # in privacyIDEA.
        # This is answered and the RADIUS server sends an AccessAccept
        r = add_radius(identifier="myserver", server="1.2.3.4",
                       secret="testing123", dictionary=DICT_FILE)
        self.assertTrue(r > 0)
        token = init_token({"type": "radius",
                            "radius.identifier": "myserver",
                            "radius.local_checkpin": False,
                            "radius.user": u"nönäscii"})

        # Check if the remote PIN would create a RADIUS challenge
        state1 = [b"123456"]
        radiusmock.setdata(timeout=False, response=radiusmock.AccessChallenge,
                           response_data={"State": state1,
                                          "Reply_Message": ["Please provide more information."]})
        opts = {}
        r = token.is_challenge_request("some_remote_value", options=opts)
        self.assertTrue(r)
        self.assertEqual(opts.get("radius_message"), "Please provide more information.")
        self.assertEqual(opts.get("radius_result"), radiusmock.AccessChallenge)
        self.assertEqual(opts.get("radius_state"), state1[0])

        # Creating the challenge within privacyIDEA
        r, message, transaction_id, _attr = token.create_challenge(options=opts)
        self.assertTrue(r)
        self.assertEqual(message, "Please provide more information.")

        # Check if a challenge is created
        chals = get_challenges(token.token.serial)
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # Checking, if this is the answer attempt to a challenge
        r = token.is_challenge_response("some_response", options={"transaction_id": transaction_id})
        self.assertTrue(r)

        # Check what happens if the RADIUS server rejects the response
        radiusmock.setdata(timeout=False, response=radiusmock.AccessReject)
        r = token.check_challenge_response(passw="some_response",
                                           options={"transaction_id": transaction_id})
        self.assertLess(r, 0)

        # Now checking the response to the challenge and we issue a RADIUS request
        radiusmock.setdata(timeout=False, response=radiusmock.AccessAccept)
        r = token.check_challenge_response(passw="some_response",
                                           options={"transaction_id": transaction_id})
        self.assertGreaterEqual(r, 0)
 def has_further_challenge(self, options=None):
     """
     Check if there are still more tokens to be authenticated
     :param options: Options dict
     :return: True, if further challenge is required.
     """
     transaction_id = options.get('transaction_id')
     challengeobject_list = get_challenges(serial=self.token.serial,
                                           transaction_id=transaction_id)
     if len(challengeobject_list) == 1:
         remaining_realms = self._get_remaining_realms(options.get("data", {}))
         if remaining_realms:
             options["data"] = json.dumps(options.get("data", {}))
             options["message"] = "Remaining tokens: {0!s}".format(remaining_realms)
             return True
     return False
Exemple #11
0
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if there is a matching question for the given
        passw and also verifies if the answer is correct.

        It then returns the the otp_counter = 1

        :param user: the requesting user
        :type user: User object
        :param passw: the password - in fact it is the answer to the question
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return 1 if the answer to the question is correct, -1 otherwise.
        :rtype: int
        """
        options = options or {}
        r_success = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    # challenge is still valid
                    if self.check_answer(passw, challengeobject) > 0:
                        r_success = 1
                        # Set valid OTP to true. We must not delete the challenge now,
                        # Since we need it for further mutlichallenges
                        challengeobject.set_otp_status(True)
                        log.debug("The presented answer was correct.")
                        break
                    else:
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return r_success
Exemple #12
0
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if the given response is the PIN + OTP of one of the
        remaining tokens.
        In case of success it then returns ``1``

        :param user: the requesting user
        :type user: User object
        :param passw: the password: PIN + OTP
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return 1 if the answer to the challenge is correct, -1 otherwise.
        :rtype: int
        """
        options = options or {}
        r_success = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    # challenge is still valid
                    used_tokens = json.loads(challengeobject_list[0].data
                                             or json.dumps({}))
                    remaining_realms = self._get_remaining_realms(used_tokens)
                    r_success = self._authenticate_remaining_realms(
                        passw, remaining_realms, used_tokens, options)

                    if r_success:
                        challengeobject.set_otp_status(True)
                    if not r_success:
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return r_success
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if there is a matching question for the given
        passw and also verifies if the answer is correct.

        It then returns the the otp_counter = 1

        :param user: the requesting user
        :type user: User object
        :param passw: the password - in fact it is the answer to the question
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return otp_counter. If -1, challenge does not match
        :rtype: int
        """
        options = options or {}
        otp_counter = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id', None)
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(serial=self.token.serial,
                                                  transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    # challenge is still valid
                    otp_counter = self.check_answer(passw, challengeobject)
                    if otp_counter >= 0:
                        # We found the matching challenge, so lets return the
                        #  successful result and delete the challenge object.
                        challengeobject.delete()
                        break
                    else:
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return otp_counter
Exemple #14
0
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This function checks, if the challenge for the given transaction_id
        was marked as answered correctly.
        For this we check the otp_status of the challenge with the
        transaction_id in the database.

        We do not care about the password

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return otp_counter. If -1, challenge does not match
        :rtype: int
        """
        options = options or {}
        otp_counter = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                # check if we are still in time.
                if challengeobject.is_valid():
                    _, status = challengeobject.get_otp_status()
                    if status is True:
                        # create a positive response
                        otp_counter = 1
                        # delete the challenge, should we really delete the challenge? If we do so, the information
                        # about the successful authentication could be fetched only once!
                        # challengeobject.delete()
                        break

        return otp_counter
Exemple #15
0
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This function checks, if the challenge for the given transaction_id
        was marked as answered correctly.
        For this we check the otp_status of the challenge with the
        transaction_id in the database.

        We do not care about the password

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return otp_counter. If -1, challenge does not match
        :rtype: int
        """
        options = options or {}
        otp_counter = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(serial=self.token.serial,
                                                  transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                # check if we are still in time.
                if challengeobject.is_valid():
                    _, status = challengeobject.get_otp_status()
                    if status is True:
                        # create a positive response
                        otp_counter = 1
                        # delete the challenge, should we really delete the challenge? If we do so, the information
                        # about the successful authentication could be fetched only once!
                        # challengeobject.delete()
                        break

        return otp_counter
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if there is a matching question for the given
        passw and also verifies if the answer is correct.

        It then returns the the otp_counter = 1

        :param user: the requesting user
        :type user: User object
        :param passw: the password - in fact it is the answer to the question
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transaction_id"
        :type options: dict
        :return: return otp_counter. If -1, challenge does not match
        :rtype: int
        """
        options = options or {}
        otp_counter = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    # challenge is still valid
                    otp_counter = self.check_answer(passw, challengeobject)
                    if otp_counter >= 0:
                        # We found the matching challenge, so lets return the
                        #  successful result and delete the challenge object.
                        challengeobject.delete()
                        break
                    else:
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return otp_counter
    def is_challenge_response(self, passw, user=None, options=None):
        """
        This method checks, if this is a request, that is the response to
        a previously sent challenge. But we do not query the RADIUS server.

        This is the first method in the loop ``check_token_list``.

        communication with RADIUS server: no
        modification of options: The "radius_result" key is set to None

        :param passw: password, which might be pin or pin+otp
        :type passw: string
        :param user: the requesting user
        :type user: User object
        :param options: dictionary of additional request parameters
        :type options: dict
        :return: true or false
        :rtype: bool
        """
        if options is None:
            options = {}

        challenge_response = False

        # clear the radius_result since this is the first function called in the chain
        # this value will be utilized to ensure we do not _check_radius more than once in the loop
        options.update({'radius_result': None})

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        if transaction_id:
            # get the challenges for this transaction ID
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:
                if challengeobject.is_valid():
                    challenge_response = True

        return challenge_response
    def has_further_challenge(self, options=None):
        """
        Check if there are still more questions to be asked.

        :param options: Options dict
        :return: True, if further challenge is required.
        """
        transaction_id = options.get('transaction_id')
        challengeobject_list = get_challenges(serial=self.token.serial,
                                              transaction_id=transaction_id)
        question_number = int(get_action_values_from_options(SCOPE.AUTH,
                                                             "{0!s}_{1!s}".format(self.get_class_type(),
                                                                                  QUESTACTION.NUM_QUESTIONS),
                                                             options) or 1)
        if len(challengeobject_list) == 1:
            session = int(challengeobject_list[0].session or "0") + 1
            options["session"] = u"{0!s}".format(session)
            # write the used questions to the data field
            options["data"] = challengeobject_list[0].data or ""
            if session < question_number:
                return True
        return False
    def test_04_api_authenticate_smartphone(self):
        # Test the /validate/check endpoints and the smartphone endpoint /ttype/push
        # for authentication

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        def check_firebase_params(request):
            payload = json.loads(request.body)
            # check the signature in the payload!
            data = payload.get("message").get("data")

            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**data)
            token_obj = get_tokens(serial=data.get("serial"))[0]
            pem_pubkey = token_obj.get_tokeninfo(PUBLIC_KEY_SERVER)
            pubkey_obj = load_pem_public_key(to_bytes(pem_pubkey), backend=default_backend())
            signature = b32decode(data.get("signature"))
            # If signature does not match it will raise InvalidSignature exception
            pubkey_obj.verify(signature, sign_string.encode("utf8"),
                              padding.PKCS1v15(),
                              hashes.SHA256())
            headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
            return (200, headers, json.dumps({}))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch('privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials') as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add_callback(responses.POST, 'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                          callback=check_firebase_params,
                          content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={"user": "******",
                                                     "realm": self.realm1,
                                                     "pass": "******"}):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = json.loads(res.data.decode('utf8'))
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(jsonresp.get("detail").get("serial"), tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(jsonresp.get("detail").get("message"), DEFAULT_CHALLENGE_TEXT)

        # The challenge is sent to the smartphone via the Firebase service, so we do not know
        # the challenge from the /validate/check API.
        # So lets read the challenge from the database!

        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge

        # Incomplete request fails with HTTP400
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge}):
            res = self.app.full_dispatch_request()
            self.assertEquals(res.status_code, 400)

        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Try an invalid signature first
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial[1:])
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Signed the wrong data
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong challenge
        wrong_challenge = b32encode_and_unicode(geturandom())
        wrong_sign_data = "{}|{}".format(wrong_challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": wrong_challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, empty nonce
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": "",
                                                 "signature": signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong private key
        wrong_key = rsa.generate_private_key(public_exponent=65537,
                                             key_size=4096,
                                             backend=default_backend())
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            wrong_key.sign(wrong_sign_data.encode("utf-8"),
                           padding.PKCS1v15(),
                           hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": wrong_signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Result value is still false
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "state": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertFalse(res.json['result']['value'])

        # Now the correct request
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={"serial": tokenobj.token.serial,
                                                 "nonce": challenge,
                                                 "signature": signature}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "state": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is True
            self.assertTrue(jsonresp.get("result").get("value"))
Exemple #20
0
    def test_15_multi_challenge_response_in_radius_server(self):
        # The RADIUS server issues a AccessChallenge on the first and on the
        # second request
        r = add_radius(identifier="myserver", server="1.2.3.4",
                       secret="testing123", dictionary=DICT_FILE)
        self.assertTrue(r > 0)
        token = init_token({"type": "radius",
                            "radius.identifier": "myserver",
                            "radius.local_checkpin": False,
                            "radius.user": u"nönäscii"})

        # Check if the remote PIN would create a RADIUS challenge
        state1 = [b"123456"]
        state2 = [b"999999"]
        radiusmock.setdata(timeout=False, response=radiusmock.AccessChallenge,
                           response_data={"State": state1,
                                          "Reply_Message": ["Please provide more information."]})
        opts = {}
        r = token.is_challenge_request("some_remote_value", options=opts)
        self.assertTrue(r)
        self.assertEqual(opts.get("radius_message"), "Please provide more information.")
        self.assertEqual(opts.get("radius_result"), radiusmock.AccessChallenge)
        self.assertEqual(opts.get("radius_state"), state1[0])

        # Creating the challenge within privacyIDEA
        r, message, transaction_id, _attr = token.create_challenge(options=opts)
        self.assertTrue(r)
        self.assertEqual(message, "Please provide more information.")

        # Check if a challenge is created
        chals = get_challenges(token.token.serial)
        self.assertEqual(len(chals), 1)
        self.assertEqual(chals[0].transaction_id, transaction_id)

        # Checking, if this is the answer attempt to a challenge
        r = token.is_challenge_response("some_response", options={"transaction_id": transaction_id})
        self.assertTrue(r)

        # Now checking the response to the challenge and we issue a RADIUS request
        # But the RADIUS server answers with a second AccessChallenge
        radiusmock.setdata(timeout=False, response=radiusmock.AccessChallenge,
                           response_data={"State": state2,
                                          "Reply_Message": ["Please provide even more information."]})
        opts2 = {"transaction_id": transaction_id}
        r = token.check_challenge_response(passw="some_response", options=opts2)
        # The answer might be correct, but since the RADIUS server want to get more answers, we get a -1
        self.assertEqual(r, -1)
        # but we get a new Challenge!
        self.assertEqual(opts2.get("radius_result"), radiusmock.AccessChallenge)
        self.assertEqual(opts2.get("radius_state"), state2[0])
        self.assertEqual(opts2.get("radius_message"), "Please provide even more information.")
        transaction_id2 = opts2.get("transaction_id")

        # Finally we send the last auth request
        opts3 = {"transaction_id": transaction_id2}
        radiusmock.setdata(timeout=False, response=radiusmock.AccessAccept)
        r = token.check_challenge_response(passw="some_other_response",
                                           options=opts3)
        # The answer is correct,
        self.assertEqual(r, 1)
        # and we do not get a new challenge, it is the same as before
        self.assertEqual(opts3.get("radius_result"), radiusmock.AccessAccept)
        transaction_id3 = opts3.get("transaction_id")
        self.assertEqual(transaction_id3, transaction_id2)
    def test_01_create_token(self):
        pin = "test"
        token = init_token({"type": "ocra",
                            "pin": pin,
                            "serial": "OCRA1",
                            "user": "******",
                            "realm": self.realm1,
                            "otpkey": KEY20
                            })
        self.assertEqual(token.type, "ocra")

        prefix = OcraTokenClass.get_class_prefix()
        self.assertEqual(prefix, "OCRA")

        info = OcraTokenClass.get_class_info()
        self.assertEqual(info.get("type"), "ocra")

        info = OcraTokenClass.get_class_info("type")
        self.assertEqual(info, "ocra")

        # Check the challenge request
        r = token.is_challenge_request(pin)
        self.assertEqual(r, True)
        r = token.is_challenge_request(pin + "123456")
        self.assertEqual(r, False)

        # Check create_challenge
        displayTAN_challenge = "83507112  ~320,00~1399458665_G6HNVF"
        challengeQH40 = binascii.hexlify(hashlib.sha1(
            displayTAN_challenge).digest())
        r = token.create_challenge(options={"challenge": challengeQH40})
        self.assertEqual(r[0], True)
        self.assertEqual(r[1], "Please answer the challenge")

        # answer the challenge wrongly
        r = token.verify_response(passw="00065298", challenge=challengeQH40)
        self.assertTrue(r < 0, r)

        # answer the challenge
        r = token.verify_response(passw="90065298", challenge=challengeQH40)
        self.assertTrue(r > 0, r)

        # create another challenge
        displayTAN_challenge = "83507112  ~320,00~1399458665_G6HNVF"
        challengeQH40 = binascii.hexlify(hashlib.sha1(
            displayTAN_challenge).digest())
        r = token.create_challenge(options={"challenge": challengeQH40})
        self.assertEqual(r[0], True)
        self.assertEqual(r[1], "Please answer the challenge")
        transaction_id = r[2]

        # answer the challenge wrongly using check_challenge_response
        r = token.check_challenge_response(passw="00065298", options={
            "transaction_id": transaction_id
        })
        self.assertEqual(r, -1)

        # assert there is still one challenge
        self.assertEqual(len(get_challenges(serial="OCRA1", transaction_id=transaction_id)), 1)

        # answer the challenge correctly using check_challenge_response
        r = token.check_challenge_response(passw="90065298", options={
            "transaction_id": transaction_id
        })
        self.assertTrue(r > 0, r)

        # assert there is no challenge anymore
        self.assertEqual(len(get_challenges(serial="OCRA1", transaction_id=transaction_id)), 0)
    def test_03b_api_authenticate_client(self):
        # Test the /validate/check endpoints without the smartphone endpoint /ttype/push
        self.setUp_user_realms()

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                body="""{}""",
                content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

            # Our ServiceAccountCredentials mock has not been called because we use a cached token
            self.assertEqual(len(mySA.from_json_keyfile_name.mock_calls), 0)
            self.assertIn(FIREBASE_FILE,
                          get_app_local_store()["firebase_token"])

        # The mobile device has not communicated with the backend, yet.
        # The user is not authenticated!
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = res.json
            # Result-Value is false, the user has not answered the challenge, yet
            self.assertFalse(jsonresp.get("result").get("value"))

        # As the challenge has not been answered yet, the /validate/polltransaction endpoint returns false
        with self.app.test_request_context(
                '/validate/polltransaction',
                method='GET',
                data={'transaction_id': transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 200)
            self.assertTrue(res.json["result"]["status"])
            self.assertFalse(res.json["result"]["value"])

        # Now the smartphone communicates with the backend and the challenge in the database table
        # is marked as answered successfully.
        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challengeobject_list[0].set_otp_status(True)

        # As the challenge has been answered, the /validate/polltransaction endpoint returns true
        with self.app.test_request_context(
                '/validate/polltransaction',
                method='GET',
                data={'transaction_id': transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 200)
            self.assertTrue(res.json["result"]["status"])
            self.assertTrue(res.json["result"]["value"])

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = res.json
            # Result-Value is True, since the challenge is marked resolved in the DB
        self.assertTrue(jsonresp.get("result").get("value"))

        # As the challenge does not exist anymore, the /validate/polltransaction endpoint returns false
        with self.app.test_request_context(
                '/validate/polltransaction',
                method='GET',
                data={'transaction_id': transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 200)
            self.assertTrue(res.json["result"]["status"])
            self.assertFalse(res.json["result"]["value"])
        self.assertEqual(get_challenges(serial=tokenobj.token.serial), [])

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        # Do single shot auth with waiting
        # Also mock time.time to be 4000 seconds in the future (exceeding the validity of myAccessTokenInfo),
        # so that we fetch a new auth token
        with mock.patch('privacyidea.lib.smsprovider.FirebaseProvider.time'
                        ) as mock_time:
            mock_time.time.return_value = time.time() + 4000

            with mock.patch(
                    'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
            ) as mySA:
                # alternative: side_effect instead of return_value
                mySA.from_json_keyfile_name.return_value = myCredentials(
                    myAccessTokenInfo("my_new_bearer_token"))

                # add responses, to simulate the communication to firebase
                responses.add(
                    responses.POST,
                    'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                    body="""{}""",
                    content_type="application/json")

                # In two seconds we need to run an update on the challenge table.
                Timer(2, self.mark_challenge_as_accepted).start()

                set_policy("push1",
                           scope=SCOPE.AUTH,
                           action="{0!s}=20".format(PUSH_ACTION.WAIT))
                # Send the first authentication request to trigger the challenge
                with self.app.test_request_context('/validate/check',
                                                   method='POST',
                                                   data={
                                                       "user": "******",
                                                       "realm": self.realm1,
                                                       "pass": "******"
                                                   }):
                    res = self.app.full_dispatch_request()
                    self.assertTrue(res.status_code == 200, res)
                    jsonresp = res.json
                    # We successfully authenticated! YEAH!
                    self.assertTrue(jsonresp.get("result").get("value"))
                    self.assertTrue(jsonresp.get("result").get("status"))
                    self.assertEqual(
                        jsonresp.get("detail").get("serial"),
                        tokenobj.token.serial)
                delete_policy("push1")

            # Our ServiceAccountCredentials mock has been called once because we fetched a new token
            self.assertEqual(len(mySA.from_json_keyfile_name.mock_calls), 1)
            self.assertIn(FIREBASE_FILE,
                          get_app_local_store()["firebase_token"])
            self.assertEqual(
                get_app_local_store()["firebase_token"]
                [FIREBASE_FILE].access_token, "my_new_bearer_token")

        # Authentication fails, if the push notification is not accepted within the configured time
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                body="""{}""",
                content_type="application/json")

            set_policy("push1",
                       scope=SCOPE.AUTH,
                       action="{0!s}=1".format(PUSH_ACTION.WAIT))
            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = res.json
                # We fail to authenticate! Oh No!
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
            delete_policy("push1")
    def test_03_api_authenticate_client(self):
        # Test the /validate/check endpoints without the smartphone endpoint /ttype/push
        self.setUp_user_realms()

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch('privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials') as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(responses.POST, 'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                          body="""{}""",
                          content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={"user": "******",
                                                     "realm": self.realm1,
                                                     "pass": "******"}):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = json.loads(res.data.decode('utf8'))
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(jsonresp.get("detail").get("serial"), tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(jsonresp.get("detail").get("message"), DEFAULT_CHALLENGE_TEXT)

        # The mobile device has not communicated with the backend, yet.
        # The user is not authenticated!
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "transaction_id": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is false, the user has not answered the challenge, yet
            self.assertFalse(jsonresp.get("result").get("value"))

        # Now the smartphone communicates with the backend and the challenge in the database table
        # is marked as answered successfully.
        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challengeobject_list[0].set_otp_status(True)

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={"user": "******",
                                                 "realm": self.realm1,
                                                 "pass": "",
                                                 "state": transaction_id}):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is True, since the challenge is marked resolved in the DB
        self.assertTrue(jsonresp.get("result").get("value"))
    def api_endpoint(request, g):
        """
        This provides a function to be plugged into the API endpoint
        /ttype/<tokentype> which is defined in api/ttype.py
        See :ref:`rest_ttype`.

        :param request: The Flask request
        :param g: The Flask global object g
        :return: Flask Response or text
        """
        params = request.all_data
        action = getParam(params, "action", optional) or \
                 API_ACTIONS.AUTHENTICATION
        if action not in API_ACTIONS.ALLOWED_ACTIONS:
            raise ParameterError("Allowed actions are {0!s}".format(
                                 API_ACTIONS.ALLOWED_ACTIONS))

        if action == API_ACTIONS.METADATA:
            session = getParam(params, "session", required)
            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()

            service_identifier = get_from_config("tiqr.serviceIdentifier") or\
                                 "org.privacyidea"
            ocrasuite = get_from_config("tiqr.ocrasuite") or OCRA_DEFAULT_SUITE
            service_displayname = get_from_config("tiqr.serviceDisplayname") or \
                                  "privacyIDEA"
            reg_server = get_from_config("tiqr.regServer")
            auth_server = get_from_config("tiqr.authServer") or reg_server
            logo_url = get_from_config("tiqr.logoUrl")

            service = {"displayName": service_displayname,
                       "identifier": service_identifier,
                       "logoUrl": logo_url,
                       "infoUrl": "https://www.privacyidea.org",
                       "authenticationUrl":
                           "{0!s}".format(auth_server),
                       "ocraSuite": ocrasuite,
                       "enrollmentUrl":
                           "{0!s}?action={1!s}&session={2!s}&serial={3!s}".format(
                               reg_server,
                               API_ACTIONS.ENROLLMENT,
                               session, serial)
                       }
            identity = {"identifier": user_identifier,
                        "displayName": user_displayname
                        }

            res = {"service": service,
                   "identity": identity
                   }

            return "json", res

        elif action == API_ACTIONS.ENROLLMENT:
            """
            operation: register
            secret: HEX
            notificationType: GCM
            notificationAddress: ...
            language: de
            session:
            serial:
            """
            res = "Fail"
            serial = getParam(params, "serial", required)
            session = getParam(params, "session", required)
            secret = getParam(params, "secret", required)
            # 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")

            return "plain", res
        elif action == API_ACTIONS.AUTHENTICATION:
            res = "FAIL"
            userId = getParam(params, "userId", required)
            session = getParam(params, "sessionKey", required)
            passw = getParam(params, "response", required)
            operation = getParam(params, "operation", required)
            res = "INVALID_CHALLENGE"
            # The sessionKey is stored in the db_challenge.transaction_id
            # We need to get the token serial for this sessionKey
            challenges = get_challenges(transaction_id=session)
            # We found exactly one challenge
            if (len(challenges) == 1 and challenges[0].is_valid() and
                        challenges[0].otp_valid is False):
                # 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)

            cleanup_challenges()

            return "plain", res
Exemple #25
0
def generic_challenge_response_reset_pin(wrapped_function, *args, **kwds):
    """
    Check if the authentication was successful, but if the token needs to reset
    its PIN.

    Conditions: To do so we check for "next_pin_change" in the tokeninfo data. This
    is however easily done using token.is_pin_change().

    Policies: A policy defines, if this PIN reset functionality should be active
    at all. scope=AUTH, action=CHANGE_PIN_VIA_VALIDATE

    args are:
    :param tokenobject_list: The list of all the tokens of the user, that will be checked
    :param passw: The password presented in the authentication. We need this for the PIN reset.

    kwds are:
    :param options: options dictionary containing g
    :param user: The user_obj
    """

    # Before we call the wrapped function, we need to check, if we have a generic challenge
    # for the given transaction_id and if the token serial matches a given token
    options = kwds.get("options") or {}
    user_obj = kwds.get("user")
    transaction_id = options.get("transaction_id") or options.get("state")
    if transaction_id:
        challenges = get_challenges(transaction_id=transaction_id,
                                    challenge=CHALLENGE_TYPE.PIN_RESET)
        if len(challenges) == 1:
            challenge = challenges[0]
            # check if challenge matches a token and if it is valid
            token_obj = next(t for t in args[0]
                             if t.token.serial == challenge.serial)
            if token_obj:
                # Then either verify the PIN or set the PIN the first time. The
                # PIN from the 1st response is stored in challenge.data
                if challenge.data:
                    # Verify the password
                    if verify_pass_hash(args[1], challenge.data):
                        g = options.get("g")
                        challenge.set_otp_status(True)
                        token_obj.challenge_janitor()
                        # Success, set new PIN and return success
                        token_obj.set_pin(args[1])
                        pinpol = Match.token(
                            g,
                            scope=SCOPE.ENROLL,
                            action=ACTION.CHANGE_PIN_EVERY,
                            token_obj=token_obj).action_values(unique=True)
                        # Set a new next_pin_change
                        if pinpol:
                            # Set a new next pin change
                            token_obj.set_next_pin_change(diff=list(pinpol)[0])
                        else:
                            # Obviously the admin removed the policy for changing pins,
                            # so we will not require to change the PIN again
                            token_obj.del_tokeninfo("next_pin_change")
                        return True, {
                            "message": "PIN successfully set.",
                            "serial": token_obj.token.serial
                        }
                    else:
                        return False, {
                            "serial": token_obj.token.serial,
                            "message": "PINs do not match"
                        }
                else:
                    # The PIN is presented the first time.
                    # Verify if the PIN adheres to the PIN policies. This is always in the normal user context
                    g = options.get("g")
                    g.logged_in_user = {"role": SCOPE.USER}
                    if user_obj:
                        # check_pin below originally works for logged in users, since only logged in users
                        # are allowed to change the pin. So we need to construct a logged_in_user object, otherwise
                        # check_pin would fail.
                        g.logged_in_user["username"] = user_obj.login
                        g.logged_in_user["realm"] = user_obj.realm
                    check_pin(g, args[1], token_obj.token.tokentype, user_obj)
                    # We need to ask for a 2nd time
                    challenge.set_otp_status(True)
                    seed = get_rand_digit_str(SEED_LENGTH)
                    reply_dict = _create_pin_reset_challenge(
                        token_obj, _("Please enter the new PIN again"),
                        pass_hash(args[1]))
                    return False, reply_dict

    success, reply_dict = wrapped_function(*args, **kwds)

    # After a successful authentication, we might start the PIN change process
    if success and reply_dict.get("pin_change"):
        g = options.get("g")
        # Determine the realm by the serial
        serial = reply_dict.get("serial")
        # The tokenlist can contain more than one token. So we get the matching token object
        token_obj = next(t for t in args[0] if t.token.serial == serial)
        if g and Match.token(g,
                             scope=SCOPE.AUTH,
                             action=ACTION.CHANGE_PIN_VIA_VALIDATE,
                             token_obj=token_obj).any():
            reply_dict = _create_pin_reset_challenge(
                token_obj, _("Please enter a new PIN"))
            return False, reply_dict

    return success, reply_dict
Exemple #26
0
    def create_challenge(self, transactionid=None, options=None):
        """
        This method creates a challenge, which is submitted to the user.
        The submitted challenge will be preserved in the challenge
        database.

        If no transaction id is given, the system will create a transaction
        id and return it, so that the response can refer to this transaction.

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :type options: dict
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional challenge ``reply_dict``, which are displayed in the JSON challenges response.
        """
        options = options or {}
        message = get_action_values_from_options(
            SCOPE.AUTH, "{0!s}_{1!s}".format(
                self.get_class_type(), ACTION.CHALLENGETEXT), options) or _(
                    u'Please confirm with your U2F token ({0!s})').format(
                        self.token.description)

        validity = int(get_from_config('DefaultChallengeValidityTime', 120))
        tokentype = self.get_tokentype().lower()
        lookup_for = tokentype.capitalize() + 'ChallengeValidityTime'
        validity = int(get_from_config(lookup_for, validity))

        # if a transaction id is given, check if there are other u2f token and
        # reuse the challenge
        challenge = None
        if transactionid:
            for c in get_challenges(transaction_id=transactionid):
                if get_tokens(serial=c.serial,
                              tokentype=self.get_class_type(),
                              count=True):
                    challenge = c.challenge
                    break

        if not challenge:
            nonce = geturandom(32)
            challenge = hexlify_and_unicode(nonce)
        else:
            nonce = binascii.unhexlify(challenge)

        # Create the challenge in the database
        db_challenge = Challenge(self.token.serial,
                                 transaction_id=transactionid,
                                 challenge=challenge,
                                 data=None,
                                 session=options.get("session"),
                                 validitytime=validity)
        db_challenge.save()
        sec_object = self.token.get_otpkey()
        key_handle_hex = sec_object.getKey()
        key_handle_bin = binascii.unhexlify(key_handle_hex)
        key_handle_url = url_encode(key_handle_bin)
        challenge_url = url_encode(nonce)
        u2f_sign_request = {
            "appId": self.get_tokeninfo("appId"),
            "version": U2F_Version,
            "challenge": challenge_url,
            "keyHandle": key_handle_url
        }

        image_url = IMAGES.get(self.token.description.lower().split()[0], "")
        reply_dict = {
            "attributes": {
                "u2fSignRequest": u2f_sign_request,
                "hideResponseInput":
                self.client_mode != CLIENTMODE.INTERACTIVE,
                "img": image_url
            },
            "image": image_url
        }

        return True, message, db_challenge.transaction_id, reply_dict
Exemple #27
0
    def _api_endpoint_post(cls, request_data):
        """ Handle all POST requests to the api endpoint

        :param request_data: Dictionary containing the parameters of the request
        :type request_data: dict
        :returns: The result of handling the request and a dictionary containing
                  the details of the request handling
        :rtype: (bool, dict)
        """
        details = {}
        result = False

        serial = getParam(request_data, "serial", optional=False)
        if all(k in request_data for k in ("fbtoken", "pubkey")):
            log.debug("Do the 2nd step of the enrollment.")
            try:
                token_obj = get_one_token(
                    serial=serial,
                    tokentype="push",
                    rollout_state=ROLLOUTSTATE.CLIENTWAIT)
                token_obj.update(request_data)
            except ResourceNotFoundError:
                raise ResourceNotFoundError(
                    "No token with this serial number "
                    "in the rollout state 'clientwait'.")
            init_detail_dict = request_data

            details = token_obj.get_init_detail(init_detail_dict)
            result = True
        elif all(k in request_data for k in ("nonce", "signature")):
            log.debug(
                "Handling the authentication response from the smartphone.")
            challenge = getParam(request_data, "nonce")
            signature = getParam(request_data, "signature")

            # get the token_obj for the given serial:
            token_obj = get_one_token(serial=serial, tokentype="push")
            pubkey_obj = _build_verify_object(
                token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
            # Do the 2nd step of the authentication
            # Find valid challenges
            challengeobject_list = get_challenges(serial=serial,
                                                  challenge=challenge)

            if challengeobject_list:
                # There are valid challenges, so we check this signature
                for chal in challengeobject_list:
                    # verify the signature of the nonce
                    sign_data = u"{0!s}|{1!s}".format(challenge, serial)
                    try:
                        pubkey_obj.verify(b32decode(signature),
                                          sign_data.encode("utf8"),
                                          padding.PKCS1v15(), hashes.SHA256())
                        # The signature was valid
                        log.debug(
                            "Found matching challenge {0!s}.".format(chal))
                        chal.set_otp_status(True)
                        chal.save()
                        result = True
                    except InvalidSignature as _e:
                        pass
        elif all(k in request_data
                 for k in ('new_fb_token', 'timestamp', 'signature')):
            timestamp = getParam(request_data, 'timestamp', optional=False)
            signature = getParam(request_data, 'signature', optional=False)
            # first check if the timestamp is in the required span
            cls._check_timestamp_in_range(timestamp, UPDATE_FB_TOKEN_WINDOW)
            try:
                tok = get_one_token(serial=serial,
                                    tokentype=cls.get_class_type())
                pubkey_obj = _build_verify_object(
                    tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
                sign_data = u"{new_fb_token}|{serial}|{timestamp}".format(
                    **request_data)
                pubkey_obj.verify(b32decode(signature),
                                  sign_data.encode("utf8"), padding.PKCS1v15(),
                                  hashes.SHA256())
                # If the timestamp and signature are valid we update the token
                tok.add_tokeninfo('firebase_token',
                                  request_data['new_fb_token'])
                result = True
            except (ResourceNotFoundError, ParameterError, TypeError,
                    InvalidSignature, ConfigAdminError, BinasciiError) as e:
                # to avoid disclosing information we always fail with an invalid
                # signature error even if the token with the serial could not be found
                log.debug('{0!s}'.format(traceback.format_exc()))
                log.info('The following error occurred during the signature '
                         'check: "{0!r}"'.format(e))
                raise privacyIDEAError('Could not verify signature!')
        else:
            raise ParameterError("Missing parameters!")

        return result, details
Exemple #28
0
    def test_01_create_token(self):
        pin = "test"
        token = init_token({
            "type": "ocra",
            "pin": pin,
            "serial": "OCRA1",
            "user": "******",
            "realm": self.realm1,
            "otpkey": KEY20
        })
        self.assertEqual(token.type, "ocra")

        prefix = OcraTokenClass.get_class_prefix()
        self.assertEqual(prefix, "OCRA")

        info = OcraTokenClass.get_class_info()
        self.assertEqual(info.get("type"), "ocra")

        info = OcraTokenClass.get_class_info("type")
        self.assertEqual(info, "ocra")

        # Check the challenge request
        r = token.is_challenge_request(pin)
        self.assertEqual(r, True)
        r = token.is_challenge_request(pin + "123456")
        self.assertEqual(r, False)

        # Check create_challenge
        displayTAN_challenge = "83507112  ~320,00~1399458665_G6HNVF"
        challengeQH40 = binascii.hexlify(
            hashlib.sha1(displayTAN_challenge).digest())
        r = token.create_challenge(options={"challenge": challengeQH40})
        self.assertEqual(r[0], True)
        self.assertEqual(r[1], "Please answer the challenge")

        # answer the challenge wrongly
        r = token.verify_response(passw="00065298", challenge=challengeQH40)
        self.assertTrue(r < 0, r)

        # answer the challenge
        r = token.verify_response(passw="90065298", challenge=challengeQH40)
        self.assertTrue(r > 0, r)

        # create another challenge
        displayTAN_challenge = "83507112  ~320,00~1399458665_G6HNVF"
        challengeQH40 = binascii.hexlify(
            hashlib.sha1(displayTAN_challenge).digest())
        r = token.create_challenge(options={"challenge": challengeQH40})
        self.assertEqual(r[0], True)
        self.assertEqual(r[1], "Please answer the challenge")
        transaction_id = r[2]

        # answer the challenge wrongly using check_challenge_response
        r = token.check_challenge_response(
            passw="00065298", options={"transaction_id": transaction_id})
        self.assertEqual(r, -1)

        # assert there is still one challenge
        self.assertEqual(
            len(get_challenges(serial="OCRA1", transaction_id=transaction_id)),
            1)

        # answer the challenge correctly using check_challenge_response
        r = token.check_challenge_response(
            passw="90065298", options={"transaction_id": transaction_id})
        self.assertTrue(r > 0, r)

        # assert there is no challenge anymore
        self.assertEqual(
            len(get_challenges(serial="OCRA1", transaction_id=transaction_id)),
            0)
Exemple #29
0
    def api_endpoint(cls, request, g):
        """
        This provides a function which is called by the API endpoint
        /ttype/push which is defined in api/ttype.py

        The method returns
            return "json", {}

        This endpoint is used for the 2nd enrollment step of the smartphone.
        Parameters sent:
            * serial
            * fbtoken
            * pubkey

        This endpoint is also used, if the smartphone sends the signed response
        to the challenge during authentication
        Parameters sent:
            * serial
            * nonce (which is the challenge)
            * signature (which is the signed nonce)


        :param request: The Flask request
        :param g: The Flask global object g
        :return: dictionary
        """
        details = {}
        result = False
        serial = getParam(request.all_data, "serial", optional=False)

        if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data:
            # Do the 2nd step of the enrollment
            try:
                token_obj = get_one_token(serial=serial,
                                          tokentype="push",
                                          rollout_state="clientwait")
                token_obj.update(request.all_data)
            except ResourceNotFoundError:
                raise ResourceNotFoundError(
                    "No token with this serial number in the rollout state 'clientwait'."
                )
            init_detail_dict = request.all_data

            details = token_obj.get_init_detail(init_detail_dict)
            result = True
        elif serial and "nonce" in request.all_data and "signature" in request.all_data:
            challenge = getParam(request.all_data, "nonce")
            serial = getParam(request.all_data, "serial")
            signature = getParam(request.all_data, "signature")

            # get the token_obj for the given serial:
            token_obj = get_one_token(serial=serial, tokentype="push")
            pubkey_pem = token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)
            # The public key of the smartphone was probably sent as urlsafe:
            pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/")
            # The public key was sent without any header
            pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC KEY-----".format(
                pubkey_pem.strip().replace(" ", "+"))
            # Do the 2nd step of the authentication
            # Find valid challenges
            challengeobject_list = get_challenges(serial=serial,
                                                  challenge=challenge)

            if challengeobject_list:
                # There are valid challenges, so we check this signature
                for chal in challengeobject_list:
                    # verify the signature of the nonce
                    pubkey_obj = serialization.load_pem_public_key(
                        to_bytes(pubkey_pem), default_backend())
                    sign_data = u"{0!s}|{1!s}".format(challenge, serial)
                    try:
                        pubkey_obj.verify(b32decode(signature),
                                          sign_data.encode("utf8"),
                                          padding.PKCS1v15(), hashes.SHA256())
                        # The signature was valid
                        chal.set_otp_status(True)
                        result = True
                    except InvalidSignature as e:
                        pass

        else:
            raise ParameterError("Missing parameters!")

        return "json", prepare_result(result, details=details)
    def api_endpoint(cls, request, g):
        """
        This provides a function which is called by the API endpoint
        ``/ttype/push`` which is defined in :doc:`../../api/ttype`

        The method returns a tuple ``("json", {})``

        This endpoint provides several functionalities:

        - It is used for the 2nd enrollment step of the smartphone.
          It accepts the following parameters:

            .. sourcecode:: http

              POST /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<token serial>
              fbtoken=<firebase token>
              pubkey=<public key>

        - It is also used when the smartphone sends the signed response
          to the challenge during authentication. The following parameters ar accepted:

            .. sourcecode:: http

              POST /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<token serial>
              nonce=<the actual challenge>
              signature=<the signed nonce>

        - And it also acts as an endpoint for polling challenges:

            .. sourcecode:: http

              GET /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<tokenserial>
              timestamp=<timestamp>
              signature=SIGNATURE(<tokenserial>|<timestamp>)

          More on polling can be found here: https://github.com/privacyidea/privacyidea/wiki/concept%3A-pushtoken-poll

        :param request: The Flask request
        :param g: The Flask global object g
        :return: The json string representing the result dictionary
        :rtype: tuple("json", str)
        """
        details = {}
        result = False

        if request.method == 'POST':
            serial = getParam(request.all_data, "serial", optional=False)
            if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data:
                log.debug("Do the 2nd step of the enrollment.")
                try:
                    token_obj = get_one_token(serial=serial,
                                              tokentype="push",
                                              rollout_state="clientwait")
                    token_obj.update(request.all_data)
                except ResourceNotFoundError:
                    raise ResourceNotFoundError(
                        "No token with this serial number "
                        "in the rollout state 'clientwait'.")
                init_detail_dict = request.all_data

                details = token_obj.get_init_detail(init_detail_dict)
                result = True
            elif serial and "nonce" in request.all_data and "signature" in request.all_data:
                log.debug(
                    "Handling the authentication response from the smartphone."
                )
                challenge = getParam(request.all_data, "nonce")
                serial = getParam(request.all_data, "serial")
                signature = getParam(request.all_data, "signature")

                # get the token_obj for the given serial:
                token_obj = get_one_token(serial=serial, tokentype="push")
                pubkey_obj = _build_verify_object(
                    token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
                # Do the 2nd step of the authentication
                # Find valid challenges
                challengeobject_list = get_challenges(serial=serial,
                                                      challenge=challenge)

                if challengeobject_list:
                    # There are valid challenges, so we check this signature
                    for chal in challengeobject_list:
                        # verify the signature of the nonce
                        sign_data = u"{0!s}|{1!s}".format(challenge, serial)
                        try:
                            pubkey_obj.verify(b32decode(signature),
                                              sign_data.encode("utf8"),
                                              padding.PKCS1v15(),
                                              hashes.SHA256())
                            # The signature was valid
                            log.debug(
                                "Found matching challenge {0!s}.".format(chal))
                            chal.set_otp_status(True)
                            chal.save()
                            result = True
                        except InvalidSignature as _e:
                            pass

            else:
                raise ParameterError("Missing parameters!")
        elif request.method == 'GET':
            # This is only used for polling
            # By default we allow polling if the policy is not set.
            allow_polling = get_action_values_from_options(
                SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING,
                options={'g': g}) or PushAllowPolling.ALLOW
            if allow_polling == PushAllowPolling.DENY:
                raise PolicyError('Polling not allowed!')
            serial = getParam(request.all_data, "serial", optional=False)
            timestamp = getParam(request.all_data, 'timestamp', optional=False)
            signature = getParam(request.all_data, 'signature', optional=False)
            # first check if the timestamp is in the required span
            try:
                ts = isoparse(timestamp)
            except (ValueError, TypeError) as _e:
                log.debug('{0!s}'.format(traceback.format_exc()))
                raise privacyIDEAError(
                    'Could not parse timestamp {0!s}. '
                    'ISO-Format required.'.format(timestamp))
            # TODO: make time delta configurable
            td = timedelta(minutes=POLL_TIME_WINDOW)
            # We don't know if the passed timestamp is timezone aware. If no
            # timezone is passed, we assume UTC
            if ts.tzinfo:
                now = datetime.now(utc)
            else:
                now = datetime.utcnow()
            if not (now - td <= ts <= now + td):
                raise privacyIDEAError(
                    'Timestamp {0!s} not in valid range.'.format(timestamp))
            # now check the signature
            # first get the token
            try:
                tok = get_one_token(serial=serial,
                                    tokentype=cls.get_class_type())
                # If the push_allow_polling policy is set to "token" we also
                # need to check the POLLING_ALLOWED tokeninfo. If it evaluated
                # to 'False', polling is not allowed for this token. If the
                # tokeninfo value evaluates to 'True' or is not set at all,
                # polling is allowed for this token.
                if allow_polling == PushAllowPolling.TOKEN:
                    if not is_true(
                            tok.get_tokeninfo(POLLING_ALLOWED,
                                              default='True')):
                        log.debug(
                            'Polling not allowed for pushtoken {0!s} due to '
                            'tokeninfo.'.format(serial))
                        raise PolicyError('Polling not allowed!')

                pubkey_obj = _build_verify_object(
                    tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
                sign_data = u"{serial}|{timestamp}".format(**request.all_data)
                pubkey_obj.verify(b32decode(signature),
                                  sign_data.encode("utf8"), padding.PKCS1v15(),
                                  hashes.SHA256())
                # The signature was valid now check for an open challenge
                # we need the private server key to sign the smartphone data
                pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER)
                # we also need the FirebaseGateway for this token
                fb_identifier = tok.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
                if not fb_identifier:
                    raise ResourceNotFoundError(
                        'The pushtoken {0!s} has no Firebase configuration '
                        'assigned.'.format(serial))
                fb_gateway = create_sms_instance(fb_identifier)
                options = {'g': g}
                challenges = []
                challengeobject_list = get_challenges(serial=serial)
                for chal in challengeobject_list:
                    # check if the challenge is active and not already answered
                    _cnt, answered = chal.get_otp_status()
                    if not answered and chal.is_valid():
                        # then return the necessary smartphone data to answer
                        # the challenge
                        sp_data = _build_smartphone_data(
                            serial, chal.challenge, fb_gateway, pem_privkey,
                            options)
                        challenges.append(sp_data)
                # return the challenges as a list in the result value
                result = challenges
            except (ResourceNotFoundError, ParameterError, InvalidSignature,
                    ConfigAdminError, BinasciiError) as e:
                # to avoid disclosing information we always fail with an invalid
                # signature error even if the token with the serial could not be found
                log.debug('{0!s}'.format(traceback.format_exc()))
                log.info('The following error occurred during the signature '
                         'check: "{0!r}"'.format(e))
                raise privacyIDEAError('Could not verify signature!')

        else:
            raise privacyIDEAError(
                'Method {0!s} not allowed in \'api_endpoint\' '
                'for push token.'.format(request.method))

        return "json", prepare_result(result, details=details)
Exemple #31
0
    def check_challenge_response(self, user=None, passw=None, options=None):
        """
        This method verifies if there is a matching challenge for the given
        passw and also verifies if the response is correct.

        It then returns 1 in case of success
        In case of failure it returns -1

        :param user: the requesting user
        :type user: User object
        :param passw: the password (pin+otp)
        :type passw: string
        :param options: additional arguments from the request, which could
                        be token specific. Usually "transactionid"
        :type options: dict
        :return: return success
        :rtype: int
        """
        options = options or {}
        r_success = -1

        # fetch the transaction_id
        transaction_id = options.get('transaction_id')
        if transaction_id is None:
            transaction_id = options.get('state')

        # get the challenges for this transaction ID
        if transaction_id is not None:
            challengeobject_list = get_challenges(
                serial=self.token.serial, transaction_id=transaction_id)

            for challengeobject in challengeobject_list:

                if challengeobject.is_valid():
                    # challenge is still valid
                    # Add the challenge to the options for check_otp
                    options["challenge"] = challengeobject.challenge
                    options["data"] = [
                        int(c) for c in challengeobject.data.split(",")
                    ]
                    # Now see if the answer is the right indexes
                    secret_string = to_unicode(
                        self.token.get_otpkey().getKey())
                    if len(options["data"]) == len(passw):
                        expected_answer = "".join(
                            [secret_string[x - 1] for x in options["data"]])
                        if passw == expected_answer:
                            r_success = 1
                            challengeobject.delete()
                            log.debug("The presented answer was correct.")
                            break
                        else:
                            log.debug("The presented answer was wrong.")
                            # increase the received_count
                            challengeobject.set_otp_status()
                    else:
                        log.debug(
                            "Length of password does not match the requested number of positions."
                        )
                        # increase the received_count
                        challengeobject.set_otp_status()

        self.challenge_janitor()
        return r_success
Exemple #32
0
    def api_endpoint(cls, request, g):
        """
        This provides a function which is called by the API endpoint
        /ttype/push which is defined in api/ttype.py

        The method returns
            return "json", {}

        This endpoint is used for the 2nd enrollment step of the smartphone.
        Parameters sent:
            * serial
            * fbtoken
            * pubkey

        This endpoint is also used, if the smartphone sends the signed response
        to the challenge during authentication
        Parameters sent:
            * serial
            * nonce (which is the challenge)
            * signature (which is the signed nonce)


        :param request: The Flask request
        :param g: The Flask global object g
        :return: dictionary
        """
        details = {}
        result = False
        serial = getParam(request.all_data, "serial", optional=False)

        if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data:
            log.debug("Do the 2nd step of the enrollment.")
            try:
                token_obj = get_one_token(serial=serial,
                                          tokentype="push",
                                          rollout_state="clientwait")
                token_obj.update(request.all_data)
            except ResourceNotFoundError:
                raise ResourceNotFoundError("No token with this serial number in the rollout state 'clientwait'.")
            init_detail_dict = request.all_data

            details = token_obj.get_init_detail(init_detail_dict)
            result = True
        elif serial and "nonce" in request.all_data and "signature" in request.all_data:
            log.debug("Handling the authentication response from the smartphone.")
            challenge = getParam(request.all_data, "nonce")
            serial = getParam(request.all_data, "serial")
            signature = getParam(request.all_data, "signature")

            # get the token_obj for the given serial:
            token_obj = get_one_token(serial=serial, tokentype="push")
            pubkey_pem = token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)
            # The public key of the smartphone was probably sent as urlsafe:
            pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/")
            # The public key was sent without any header
            pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC KEY-----".format(pubkey_pem.strip().replace(" ", "+"))
            # Do the 2nd step of the authentication
            # Find valid challenges
            challengeobject_list = get_challenges(serial=serial, challenge=challenge)

            if challengeobject_list:
                # There are valid challenges, so we check this signature
                for chal in challengeobject_list:
                    # verify the signature of the nonce
                    pubkey_obj = serialization.load_pem_public_key(to_bytes(pubkey_pem), default_backend())
                    sign_data = u"{0!s}|{1!s}".format(challenge, serial)
                    try:
                        pubkey_obj.verify(b32decode(signature),
                                          sign_data.encode("utf8"),
                                          padding.PKCS1v15(),
                                          hashes.SHA256())
                        # The signature was valid
                        log.debug("Found matching challenge {0!s}.".format(chal))
                        chal.set_otp_status(True)
                        chal.save()
                        result = True
                    except InvalidSignature as e:
                        pass

        else:
            raise ParameterError("Missing parameters!")

        return "json", prepare_result(result, details=details)
    def test_03_api_authenticate_client(self):
        # Test the /validate/check endpoints without the smartphone endpoint /ttype/push
        self.setUp_user_realms()

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/4/messages:send',
                body="""{}""",
                content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = json.loads(res.data.decode('utf8'))
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

        # The mobile device has not communicated with the backend, yet.
        # The user is not authenticated!
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is false, the user has not answered the challenge, yet
            self.assertFalse(jsonresp.get("result").get("value"))

        # Now the smartphone communicates with the backend and the challenge in the database table
        # is marked as answered successfully.
        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challengeobject_list[0].set_otp_status(True)

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = json.loads(res.data.decode('utf8'))
            # Result-Value is True, since the challenge is marked resolved in the DB
        self.assertTrue(jsonresp.get("result").get("value"))
    def test_04_api_authenticate_smartphone(self):
        # Test the /validate/check endpoints and the smartphone endpoint /ttype/push
        # for authentication

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        def check_firebase_params(request):
            payload = json.loads(request.body)
            # check the signature in the payload!
            data = payload.get("message").get("data")

            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(
                **data)
            token_obj = get_tokens(serial=data.get("serial"))[0]
            pem_pubkey = token_obj.get_tokeninfo(PUBLIC_KEY_SERVER)
            pubkey_obj = load_pem_public_key(to_bytes(pem_pubkey),
                                             backend=default_backend())
            signature = b32decode(data.get("signature"))
            # If signature does not match it will raise InvalidSignature exception
            pubkey_obj.verify(signature, sign_string.encode("utf8"),
                              padding.PKCS1v15(), hashes.SHA256())
            headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
            return (200, headers, json.dumps({}))

        # We mock the ServiceAccountCredentials, since we can not directly contact the Google API
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add_callback(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                callback=check_firebase_params,
                content_type="application/json")

            # Send the first authentication request to trigger the challenge
            with self.app.test_request_context('/validate/check',
                                               method='POST',
                                               data={
                                                   "user": "******",
                                                   "realm": self.realm1,
                                                   "pass": "******"
                                               }):
                res = self.app.full_dispatch_request()
                self.assertTrue(res.status_code == 200, res)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertTrue(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertTrue("transaction_id" in jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

            # Our ServiceAccountCredentials mock has not been called because we use a cached token
            self.assertEqual(len(mySA.from_json_keyfile_name.mock_calls), 0)
            self.assertIn(FIREBASE_FILE,
                          get_app_local_store()["firebase_token"])

        # The challenge is sent to the smartphone via the Firebase service, so we do not know
        # the challenge from the /validate/check API.
        # So lets read the challenge from the database!

        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge

        # Incomplete request fails with HTTP400
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEquals(res.status_code, 400)

        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Try an invalid signature first
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial[1:])
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        # Signed the wrong data
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong challenge
        wrong_challenge = b32encode_and_unicode(geturandom())
        wrong_sign_data = "{}|{}".format(wrong_challenge,
                                         tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(wrong_sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": wrong_challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, empty nonce
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": "",
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Correct signature, wrong private key
        wrong_key = rsa.generate_private_key(public_exponent=65537,
                                             key_size=4096,
                                             backend=default_backend())
        wrong_sign_data = "{}|{}".format(challenge, tokenobj.token.serial)
        wrong_signature = b32encode_and_unicode(
            wrong_key.sign(wrong_sign_data.encode("utf-8"), padding.PKCS1v15(),
                           hashes.SHA256()))
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": wrong_signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertFalse(res.json['result']['value'])

        # Result value is still false
        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertFalse(res.json['result']['value'])

        # Now the correct request
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        with self.app.test_request_context('/validate/check',
                                           method='POST',
                                           data={
                                               "user": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "state": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            jsonresp = res.json
            # Result-Value is True
            self.assertTrue(jsonresp.get("result").get("value"))
    def test_06_api_auth(self):
        self.setUp_user_realms()

        # get enrolled push token
        toks = get_tokens(tokentype="push")
        self.assertEqual(len(toks), 1)
        tokenobj = toks[0]

        # set PIN
        tokenobj.set_pin("pushpin")
        tokenobj.add_user(User("cornelius", self.realm1))

        # Set a loginmode policy
        set_policy("webui",
                   scope=SCOPE.WEBUI,
                   action="{}={}".format(ACTION.LOGINMODE,
                                         LOGINMODE.PRIVACYIDEA))
        # Set a PUSH_WAIT action which will be ignored by privacyIDEA
        set_policy("push1",
                   scope=SCOPE.AUTH,
                   action="{0!s}=20".format(PUSH_ACTION.WAIT))
        with mock.patch(
                'privacyidea.lib.smsprovider.FirebaseProvider.ServiceAccountCredentials'
        ) as mySA:
            # alternative: side_effect instead of return_value
            mySA.from_json_keyfile_name.return_value = myCredentials(
                myAccessTokenInfo("my_bearer_token"))

            # add responses, to simulate the communication to firebase
            responses.add(
                responses.POST,
                'https://fcm.googleapis.com/v1/projects/test-123456/messages:send',
                body="""{}""",
                content_type="application/json")

            with self.app.test_request_context(
                    '/auth',
                    method='POST',
                    data={
                        "username":
                        "******",
                        "realm":
                        self.realm1,
                        # this will be overwritted by pushtoken_disable_wait
                        PUSH_ACTION.WAIT:
                        "10",
                        "password":
                        "******"
                    }):
                res = self.app.full_dispatch_request()
                self.assertEqual(res.status_code, 401)
                jsonresp = res.json
                self.assertFalse(jsonresp.get("result").get("value"))
                self.assertFalse(jsonresp.get("result").get("status"))
                self.assertEqual(
                    jsonresp.get("detail").get("serial"),
                    tokenobj.token.serial)
                self.assertIn("transaction_id", jsonresp.get("detail"))
                transaction_id = jsonresp.get("detail").get("transaction_id")
                self.assertEqual(
                    jsonresp.get("detail").get("message"),
                    DEFAULT_CHALLENGE_TEXT)

        # Get the challenge from the database
        challengeobject_list = get_challenges(serial=tokenobj.token.serial,
                                              transaction_id=transaction_id)
        challenge = challengeobject_list[0].challenge
        # This is what the smartphone answers.
        # create the signature:
        sign_data = "{0!s}|{1!s}".format(challenge, tokenobj.token.serial)
        signature = b32encode_and_unicode(
            self.smartphone_private_key.sign(sign_data.encode("utf-8"),
                                             padding.PKCS1v15(),
                                             hashes.SHA256()))

        # We still cannot log in
        with self.app.test_request_context('/auth',
                                           method='POST',
                                           data={
                                               "username": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 401)
            self.assertFalse(res.json['result']['status'])

        # Answer the challenge
        with self.app.test_request_context('/ttype/push',
                                           method='POST',
                                           data={
                                               "serial": tokenobj.token.serial,
                                               "nonce": challenge,
                                               "signature": signature
                                           }):
            res = self.app.full_dispatch_request()
            self.assertTrue(res.status_code == 200, res)
            self.assertTrue(res.json['result']['status'])
            self.assertTrue(res.json['result']['value'])

        # We can now log in
        with self.app.test_request_context('/auth',
                                           method='POST',
                                           data={
                                               "username": "******",
                                               "realm": self.realm1,
                                               "pass": "",
                                               "transaction_id": transaction_id
                                           }):
            res = self.app.full_dispatch_request()
            self.assertEqual(res.status_code, 200)
            self.assertTrue(res.json['result']['status'])

        delete_policy("push1")
        delete_policy("webui")
Exemple #36
0
    def _api_endpoint_get(cls, g, request_data):
        """ Handle all GET requests to the api endpoint.

        Currently this is only used for polling.
        :param g: The Flask context
        :param request_data: Dictionary containing the parameters of the request
        :type request_data: dict
        :returns: Result of the polling operation, 'True' if an unanswered and
                  matching challenge exists, 'False' otherwise.
        :rtype: bool
        """
        # By default we allow polling if the policy is not set.
        allow_polling = get_action_values_from_options(
            SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING,
            options={'g': g}) or PushAllowPolling.ALLOW
        if allow_polling == PushAllowPolling.DENY:
            raise PolicyError('Polling not allowed!')
        serial = getParam(request_data, "serial", optional=False)
        timestamp = getParam(request_data, 'timestamp', optional=False)
        signature = getParam(request_data, 'signature', optional=False)
        # first check if the timestamp is in the required span
        cls._check_timestamp_in_range(timestamp, POLL_TIME_WINDOW)
        # now check the signature
        # first get the token
        try:
            tok = get_one_token(serial=serial, tokentype=cls.get_class_type())
            # If the push_allow_polling policy is set to "token" we also
            # need to check the POLLING_ALLOWED tokeninfo. If it evaluated
            # to 'False', polling is not allowed for this token. If the
            # tokeninfo value evaluates to 'True' or is not set at all,
            # polling is allowed for this token.
            if allow_polling == PushAllowPolling.TOKEN:
                if not is_true(
                        tok.get_tokeninfo(POLLING_ALLOWED, default='True')):
                    log.debug('Polling not allowed for pushtoken {0!s} due to '
                              'tokeninfo.'.format(serial))
                    raise PolicyError('Polling not allowed!')

            pubkey_obj = _build_verify_object(
                tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
            sign_data = u"{serial}|{timestamp}".format(**request_data)
            pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"),
                              padding.PKCS1v15(), hashes.SHA256())
            # The signature was valid now check for an open challenge
            # we need the private server key to sign the smartphone data
            pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER)
            # We need the registration URL for the challenge
            registration_url = get_action_values_from_options(
                SCOPE.ENROLL, PUSH_ACTION.REGISTRATION_URL, options={'g': g})
            if not registration_url:
                raise ResourceNotFoundError(
                    'There is no registration_url defined for the '
                    ' pushtoken {0!s}. You need to define a push_registration_url '
                    'in an enrollment policy.'.format(serial))
            options = {'g': g}
            challenges = []
            challengeobject_list = get_challenges(serial=serial)
            for chal in challengeobject_list:
                # check if the challenge is active and not already answered
                _cnt, answered = chal.get_otp_status()
                if not answered and chal.is_valid():
                    # then return the necessary smartphone data to answer
                    # the challenge
                    sp_data = _build_smartphone_data(serial, chal.challenge,
                                                     registration_url,
                                                     pem_privkey, options)
                    challenges.append(sp_data)
            # return the challenges as a list in the result value
            result = challenges
        except (ResourceNotFoundError, ParameterError, InvalidSignature,
                ConfigAdminError, BinasciiError) as e:
            # to avoid disclosing information we always fail with an invalid
            # signature error even if the token with the serial could not be found
            log.debug('{0!s}'.format(traceback.format_exc()))
            log.info('The following error occurred during the signature '
                     'check: "{0!r}"'.format(e))
            raise privacyIDEAError('Could not verify signature!')

        return result
Exemple #37
0
    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
        See :ref:`rest_ttype`.

        :param request: The Flask request
        :param g: The Flask global object g
        :return: Flask Response or text
        """
        params = request.all_data
        action = getParam(params, "action", optional) or \
                 API_ACTIONS.AUTHENTICATION
        if action not in API_ACTIONS.ALLOWED_ACTIONS:
            raise ParameterError("Allowed actions are {0!s}".format(
                                 API_ACTIONS.ALLOWED_ACTIONS))

        if action == API_ACTIONS.METADATA:
            session = getParam(params, "session", required)
            serial = getParam(params, "serial", required)
            # The user identifier is displayed in the App
            # We need to set the user ID
            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"
            ocrasuite = get_from_config("tiqr.ocrasuite") or OCRA_DEFAULT_SUITE
            service_displayname = get_from_config("tiqr.serviceDisplayname") or \
                                  "privacyIDEA"
            reg_server = get_from_config("tiqr.regServer")
            auth_server = get_from_config("tiqr.authServer") or reg_server
            logo_url = get_from_config("tiqr.logoUrl")
            info_url = get_from_config("tiqr.infoUrl") or \
                    "https://www.privacyidea.org"

            service = {"displayName": service_displayname,
                       "identifier": service_identifier,
                       "logoUrl": logo_url,
                       "infoUrl": info_url,
                       "authenticationUrl":
                           "{0!s}".format(auth_server),
                       "ocraSuite": ocrasuite,
                       "enrollmentUrl":
                           "{0!s}?action={1!s}&session={2!s}&serial={3!s}".format(
                               reg_server,
                               API_ACTIONS.ENROLLMENT,
                               session, serial)
                       }
            identity = {"identifier": user_identifier,
                        "displayName": user_displayname
                        }

            res = {"service": service,
                   "identity": identity
                   }

            return "json", res

        elif action == API_ACTIONS.ENROLLMENT:
            """
            operation: register
            secret: HEX
            notificationType: GCM
            notificationAddress: ...
            language: de
            session:
            serial:
            """
            res = "Fail"
            serial = getParam(params, "serial", required)
            session = getParam(params, "session", required)
            secret = getParam(params, "secret", required)
            # 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_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:
            res = "FAIL"
            userId = getParam(params, "userId", required)
            session = getParam(params, "sessionKey", required)
            passw = getParam(params, "response", required)
            operation = getParam(params, "operation", required)
            res = "INVALID_CHALLENGE"
            # The sessionKey is stored in the db_challenge.transaction_id
            # We need to get the token serial for this sessionKey
            challenges = get_challenges(transaction_id=session)
            # We found several challenges with the given transaction ID,
            # and some of the challenges may belong to other tokens.
            # We only handle the TiQR tokens.
            for challenge in challenges:
                if challenge.is_valid() and challenge.otp_valid is False:
                    # Challenge is still valid (time has not passed) and no
                    # correct response was given.
                    token = get_one_token(serial=challenge.serial)
                    if token.type.lower() == "tiqr":
                        # We found a TiQR token with a valid challenge with the given transaction ID
                        r = token.verify_response(
                            challenge=challenge.challenge, passw=passw)
                        if r > 0:
                            res = "OK"
                            # Mark the challenge as answered successfully.
                            challenge.set_otp_status(True)
                            # We have found a valid TiQR token transaction, we break out of the loop
                            break
                        else:
                            # Send back how may retries there are left for the token is blocked
                            token.inc_failcount()
                            fail = token.get_failcount()
                            maxfail = token.get_max_failcount()
                            res = "INVALID_RESPONSE:{0!s}".format(maxfail - fail)
                            break

            cleanup_challenges()

            return "plain", res