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 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()
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')
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
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)
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
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
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
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 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"))
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
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
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
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
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 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)
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
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")
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
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