def test_30_2step_otpkeyformat(self): serial = "2step3" db_token = Token(serial, tokentype="hotp") db_token.save() token = HotpTokenClass(db_token) token.update({ "2stepinit": "1", "2step_clientsize": "12", "hashlib": "sha512", }) self.assertEqual(token.token.rollout_state, "clientwait") self.assertEqual(token.get_tokeninfo("2step_clientsize"), "12") # fetch the server component for later tests server_component = binascii.unhexlify( token.token.get_otpkey().getKey()) # generate a 12-byte client component client_component = b'abcdefghijkl' checksum = hashlib.sha1(client_component).digest()[:4] # wrong checksum with warnings.catch_warnings(): warnings.simplefilter('ignore', category=DeprecationWarning) self.assertRaisesRegexp( ParameterError, "Incorrect checksum", token.update, { "otpkey": b32encode_and_unicode(b"\x37" + checksum[1:] + client_component).strip("="), "otpkeyformat": "base32check", }) # construct a secret token.update({ "otpkey": b32encode_and_unicode(checksum + client_component).strip("="), "otpkeyformat": "base32check", # the following values are ignored "2step_serversize": "23", "2step_difficulty": "666666", "2step_clientsize": "13" }) # check the generated secret secret = binascii.unhexlify(token.token.get_otpkey().getKey()) # check the correct lengths self.assertEqual(len(server_component), 64) # because of SHA-512 self.assertEqual(len(client_component), 12) self.assertEqual(len(secret), 64) # because of SHA-512 # check the secret has been generated according to the specification expected_secret = pbkdf2_hmac('sha1', binascii.hexlify(server_component), client_component, 10000, len(secret)) self.assertEqual(secret, expected_secret) self.assertTrue(token.token.active)
def test_30_2step_otpkeyformat(self): serial = "2step3" db_token = Token(serial, tokentype="hotp") db_token.save() token = HotpTokenClass(db_token) token.update({ "2stepinit": "1", "2step_clientsize": "12", "hashlib": "sha512", }) self.assertEqual(token.token.rollout_state, "clientwait") self.assertEqual(token.get_tokeninfo("2step_clientsize"), "12") # fetch the server component for later tests server_component = binascii.unhexlify(token.token.get_otpkey().getKey()) # generate a 12-byte client component client_component = b'abcdefghijkl' checksum = hashlib.sha1(client_component).digest()[:4] # wrong checksum self.assertRaisesRegexp( ParameterError, "Incorrect checksum", token.update, { "otpkey": b32encode_and_unicode(b"\x37" + checksum[1:] + client_component).strip("="), "otpkeyformat": "base32check", }) # construct a secret token.update({ "otpkey": b32encode_and_unicode(checksum + client_component).strip("="), "otpkeyformat": "base32check", # the following values are ignored "2step_serversize": "23", "2step_difficulty": "666666", "2step_clientsize": "13" }) # check the generated secret secret = binascii.unhexlify(token.token.get_otpkey().getKey()) # check the correct lengths self.assertEqual(len(server_component), 64) # because of SHA-512 self.assertEqual(len(client_component), 12) self.assertEqual(len(secret), 64) # because of SHA-512 # check the secret has been generated according to the specification expected_secret = pbkdf2(binascii.hexlify(server_component), client_component, 10000, len(secret)) self.assertEqual(secret, expected_secret) self.assertTrue(token.token.active)
def test_26_conversions(self): self.assertEquals(hexlify_and_unicode(u'Hallo'), u'48616c6c6f') self.assertEquals(hexlify_and_unicode(b'Hallo'), u'48616c6c6f') self.assertEquals(hexlify_and_unicode(b'\x00\x01\x02\xab'), u'000102ab') self.assertEquals(b32encode_and_unicode(u'Hallo'), u'JBQWY3DP') self.assertEquals(b32encode_and_unicode(b'Hallo'), u'JBQWY3DP') self.assertEquals(b32encode_and_unicode(b'\x00\x01\x02\xab'), u'AAAQFKY=') self.assertEquals(b64encode_and_unicode(u'Hallo'), u'SGFsbG8=') self.assertEquals(b64encode_and_unicode(b'Hallo'), u'SGFsbG8=') self.assertEquals(b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==') self.assertEquals(urlsafe_b64encode_and_unicode(u'Hallo'), u'SGFsbG8=') self.assertEquals(urlsafe_b64encode_and_unicode(b'Hallo'), u'SGFsbG8=') self.assertEquals(urlsafe_b64encode_and_unicode(b'\x00\x01\x02\xab'), u'AAECqw==') self.assertEquals(urlsafe_b64encode_and_unicode(b'\xfa\xfb\xfc\xfd\xfe\xff'), u'-vv8_f7_')
def _build_smartphone_data(serial, challenge, fb_gateway, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param fb_gateway: the gateway object containing the firebase configuration :type fb_gateway: privacyidea.lib.smsprovider.SMSProvider.ISMSProvider :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") # We send the challenge to the Firebase service url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
def _build_smartphone_data(serial, challenge, registration_url, pem_privkey, options): """ Create the dictionary to be send to the smartphone as challenge :param challenge: base32 encoded random data string :type challenge: str :param registration_url: The privacyIDEA URL, to which the Push token communicates :type registration_url: str :param options: the options dictionary :type options: dict :return: the created smartphone_data dictionary :rtype: dict """ sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": serial, "title": title, "sslverify": sslverify, "url": registration_url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) return smartphone_data
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 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 ``attributes``, which are displayed in the JSON response. """ options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT attributes = None data = None fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: challenge = b32encode_and_unicode(geturandom()) fb_gateway = create_sms_instance(fb_identifier) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) smartphone_data = _build_smartphone_data(self.token.serial, challenge, fb_gateway, pem_privkey, options) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError( "Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via firebase service." ) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def get_init_detail(self, params=None, user=None): """ to complete the token initialization some additional details should be returned, which are displayed at the end of the token initialization. This is the e.g. the enrollment URL for a Google Authenticator. """ response_detail = TokenClass.get_init_detail(self, params, user) params = params or {} tokenlabel = params.get("tokenlabel", "<s>") tokenissuer = params.get("tokenissuer", "privacyIDEA") # If the init_details contain an OTP key the OTP key # should be displayed as an enrollment URL otpkey = self.init_details.get('otpkey') # Add rollout state the response response_detail['rollout_state'] = self.token.rollout_state # Add two-step initialization parameters to response and QR code extra_data = {} if is_true(params.get("2stepinit")): twostep_parameters = self._get_twostep_parameters() extra_data.update(twostep_parameters) response_detail.update(twostep_parameters) imageurl = params.get("appimageurl") if imageurl: extra_data.update({"image": imageurl}) force_app_pin = params.get('force_app_pin') if force_app_pin: extra_data.update({'pin': True}) if otpkey: tok_type = self.type.lower() if user is not None: try: key_bin = binascii.unhexlify(otpkey) # also strip the padding =, as it will get problems with the google app. value_b32_str = b32encode_and_unicode(key_bin).strip('=') response_detail["otpkey"]["value_b32"] = value_b32_str goo_url = cr_google(key=otpkey, user=user.login, realm=user.realm, tokentype=tok_type.lower(), serial=self.get_serial(), tokenlabel=tokenlabel, hash_algo=params.get( "hashlib", "sha1"), digits=params.get("otplen", 6), period=params.get("timeStep", 30), issuer=tokenissuer, user_obj=user, extra_data=extra_data) response_detail["googleurl"] = { "description": _("URL for google " "Authenticator"), "value": goo_url, "img": create_img(goo_url, width=250) } oath_url = cr_oath(otpkey=otpkey, user=user.login, realm=user.realm, type=tok_type, serial=self.get_serial(), tokenlabel=tokenlabel, extra_data=extra_data) response_detail["oathurl"] = { "description": _("URL for" " OATH " "token"), "value": oath_url, "img": create_img(oath_url, width=250) } except Exception as ex: # pragma: no cover log.error("{0!s}".format((traceback.format_exc()))) log.error( 'failed to set oath or google url: {0!r}'.format(ex)) return response_detail
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 ``attributes``, which are displayed in the JSON response. """ res = False options = options or {} message = get_action_values_from_options( SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get( FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = { "nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url } # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format( **smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key( to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError( "Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via firebase service." ) validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def get_init_detail(self, params=None, user=None): """ to complete the token initialization some additional details should be returned, which are displayed at the end of the token initialization. This is the e.g. the enrollment URL for a Google Authenticator. """ response_detail = TokenClass.get_init_detail(self, params, user) params = params or {} tokenlabel = params.get("tokenlabel", "<s>") tokenissuer = params.get("tokenissuer", "privacyIDEA") # If the init_details contain an OTP key the OTP key # should be displayed as an enrollment URL otpkey = self.init_details.get('otpkey') # Add rollout state the response response_detail['rollout_state'] = self.token.rollout_state # Add two-step initialization parameters to response and QR code extra_data = {} if is_true(params.get("2stepinit")): twostep_parameters = self._get_twostep_parameters() extra_data.update(twostep_parameters) response_detail.update(twostep_parameters) imageurl = params.get("appimageurl") if imageurl: extra_data.update({"image": imageurl}) if otpkey: tok_type = self.type.lower() if user is not None: try: key_bin = binascii.unhexlify(otpkey) # also strip the padding =, as it will get problems with the google app. value_b32_str = b32encode_and_unicode(key_bin).strip('=') response_detail["otpkey"]["value_b32"] = value_b32_str goo_url = cr_google(key=otpkey, user=user.login, realm=user.realm, tokentype=tok_type.lower(), serial=self.get_serial(), tokenlabel=tokenlabel, hash_algo=params.get("hashlib", "sha1"), digits=params.get("otplen", 6), period=params.get("timeStep", 30), issuer=tokenissuer, user_obj=user, extra_data=extra_data) response_detail["googleurl"] = {"description": _("URL for google " "Authenticator"), "value": goo_url, "img": create_img(goo_url, width=250) } oath_url = cr_oath(otpkey=otpkey, user=user.login, realm=user.realm, type=tok_type, serial=self.get_serial(), tokenlabel=tokenlabel, extra_data=extra_data) response_detail["oathurl"] = {"description": _("URL for" " OATH " "token"), "value": oath_url, "img": create_img(oath_url, width=250) } except Exception as ex: # pragma: no cover log.error("{0!s}".format((traceback.format_exc()))) log.error('failed to set oath or google url: {0!r}'.format(ex)) return response_detail
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_01_init_token(self): set_policy( name="allow_2step", action=["hotp_2step=allow", "enrollHOTP=1", "delete"], scope=SCOPE.ADMIN, ) with self.app.test_request_context('/token/init', method='POST', data={ "type": "hotp", "genkey": "1", "2stepinit": "1" }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = res.json.get("result") self.assertTrue(result.get("status") is True, result) self.assertTrue(result.get("value") is True, result) detail = res.json.get("detail") serial = detail.get("serial") otpkey_url = detail.get("otpkey", {}).get("value") server_component = binascii.unhexlify(otpkey_url.split("/")[2]) google_url = detail["googleurl"]["value"] self.assertIn('2step_difficulty=10000', google_url) self.assertIn('2step_salt=8', google_url) self.assertIn('2step_output=20', google_url) self.assertEqual(detail['2step_difficulty'], 10000) self.assertEqual(detail['2step_salt'], 8) self.assertEqual(detail['2step_output'], 20) # Do a 2step 1st step on an already existing token using the rollover parameter will succeed with self.app.test_request_context('/token/init', method='POST', data={ "type": "hotp", "genkey": "1", "rollover": "1", "serial": serial, "2stepinit": "1" }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = res.json.get("result") self.assertTrue(result.get("status") is True, result) self.assertTrue(result.get("value") is True, result) detail = res.json.get("detail") serial = detail.get("serial") otpkey_url = detail.get("otpkey", {}).get("value") server_component = binascii.unhexlify(otpkey_url.split("/")[2]) google_url = detail["googleurl"]["value"] self.assertIn('2step_difficulty=10000', google_url) self.assertIn('2step_salt=8', google_url) self.assertIn('2step_output=20', google_url) self.assertEqual(detail['2step_difficulty'], 10000) self.assertEqual(detail['2step_salt'], 8) self.assertEqual(detail['2step_output'], 20) client_component = b"VRYSECRT" checksum = hashlib.sha1(client_component).digest()[:4] base32check_client_component = b32encode_and_unicode( checksum + client_component).strip("=") # Try to do a 2stepinit on a second step without rollover parameter will raise an error with self.app.test_request_context('/token/init', method='POST', data={ "type": "hotp", "2stepinit": "1", "serial": serial, "otpkey": base32check_client_component, "otpkeyformat": "base32check" }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 400) result = res.json.get("result") self.assertIn( '2stepinit is only to be used in the first initialization step', result.get("error").get("message")) # Invalid base32check will raise an error with self.app.test_request_context( '/token/init', method='POST', data={ "type": "hotp", "2stepinit": "1", "serial": serial, "otpkey": "A" + base32check_client_component[1:], "otpkeyformat": "base32check" }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 400) result = res.json.get("result") self.assertIn('Malformed base32check data: Incorrect checksum', result.get("error").get("message")) # Authentication does not work yet! wrong_otp_value = HmacOtp().generate(key=server_component, counter=1) with self.app.test_request_context('/validate/check', method='POST', data={ "serial": serial, "pass": wrong_otp_value }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = res.json self.assertTrue(result.get("result").get("status")) self.assertFalse(result.get("result").get("value")) self.assertEqual( result.get("detail").get("message"), u'matching 1 tokens, Token is disabled') # Now doing the correct 2nd step with self.app.test_request_context('/token/init', method='POST', data={ "type": "hotp", "serial": serial, "otpkey": base32check_client_component, "otpkeyformat": "base32check" }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = res.json.get("result") self.assertTrue(result.get("status") is True, result) self.assertTrue(result.get("value") is True, result) detail = res.json.get("detail") otpkey_url = detail.get("otpkey", {}).get("value") otpkey = otpkey_url.split("/")[2] self.assertNotIn('2step', detail) # Now try to authenticate otpkey_bin = binascii.unhexlify(otpkey) otp_value = HmacOtp().generate(key=otpkey_bin, counter=1) with self.app.test_request_context('/validate/check', method='POST', data={ "serial": serial, "pass": otp_value }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = res.json.get("result") self.assertEqual(result.get("status"), True) self.assertEqual(result.get("value"), True) # Check that the OTP key is what we expected it to be expected_secret = pbkdf2_hmac('sha1', binascii.hexlify(server_component), client_component, 10000, 20) self.assertEqual(otpkey_bin, expected_secret) with self.app.test_request_context('/token/' + serial, method='DELETE', headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) delete_policy("allow_2step")
def create_google_authenticator_url(key=None, user=None, realm=None, tokentype="hotp", period=30, serial="mylabel", tokenlabel="<s>", hash_algo="SHA1", digits="6", issuer="privacyIDEA", user_obj=None, extra_data=None): """ This creates the google authenticator URL. This url may only be 119 characters long. If the URL would be longer, we shorten the username We expect the key to be hexlified! """ extra_data = extra_data or {} # policy depends on some lib.util user_obj = user_obj or User() if tokentype.lower() == "hotp": tokentype = "hotp" counter = "counter=1&" else: counter = "" # We need realm und user to be a string realm = realm or "" user = user or "" key_bin = binascii.unhexlify(key) # also strip the padding =, as it will get problems with the google app. otpkey = b32encode_and_unicode(key_bin).strip('=') base_len = len("otpauth://{0!s}/?secret={1!s}&counter=1".format( tokentype, otpkey)) allowed_label_len = MAX_QRCODE_LEN - base_len log.debug("we have got {0!s} characters left for the token label".format( str(allowed_label_len))) # Deprecated label = tokenlabel.replace("<s>", serial).replace("<u>", user).replace("<r>", realm) label = label.format(serial=serial, user=user, realm=realm, givenname=user_obj.info.get("givenname", ""), surname=user_obj.info.get("surname", "")) issuer = issuer.format(serial=serial, user=user, realm=realm, givenname=user_obj.info.get("givenname", ""), surname=user_obj.info.get("surname", "")) label = label[0:allowed_label_len] url_label = quote(label.encode("utf-8")) url_issuer = quote(issuer.encode("utf-8")) if hash_algo.lower() != "sha1": hash_algo = "algorithm={0!s}&".format(hash_algo) else: # If the hash_algo is SHA1, we do not add it to the QR code to keep # the QR code simpler hash_algo = "" if tokentype.lower() == "totp": period = "period={0!s}&".format(period) else: period = "" return ("otpauth://{tokentype!s}/{label!s}?secret={secret!s}&" "{counter!s}{hash!s}{period!s}" "digits={digits!s}&" "issuer={issuer!s}{extra}".format( tokentype=tokentype, label=url_label, secret=otpkey, hash=hash_algo, period=period, digits=digits, issuer=url_issuer, counter=counter, extra=_construct_extra_parameters(extra_data)))
def create_google_authenticator_url(key=None, user=None, realm=None, tokentype="hotp", period=30, serial="mylabel", tokenlabel="<s>", hash_algo="SHA1", digits="6", issuer="privacyIDEA", user_obj=None, extra_data=None): """ This creates the google authenticator URL. This url may only be 119 characters long. If the URL would be longer, we shorten the username We expect the key to be hexlified! """ extra_data = extra_data or {} # policy depends on some lib.util user_obj = user_obj or User() if tokentype.lower() == "hotp": tokentype = "hotp" counter = "counter=1&" else: counter = "" # We need realm und user to be a string realm = realm or "" user = user or "" key_bin = binascii.unhexlify(key) # also strip the padding =, as it will get problems with the google app. otpkey = b32encode_and_unicode(key_bin).strip('=') base_len = len("otpauth://{0!s}/?secret={1!s}&counter=1".format(tokentype, otpkey)) allowed_label_len = MAX_QRCODE_LEN - base_len log.debug("we have got {0!s} characters left for the token label".format( str(allowed_label_len))) # Deprecated label = tokenlabel.replace("<s>", serial).replace("<u>", user).replace("<r>", realm) label = label.format(serial=serial, user=user, realm=realm, givenname=user_obj.info.get("givenname", ""), surname=user_obj.info.get("surname", "")) issuer = issuer.format(serial=serial, user=user, realm=realm, givenname=user_obj.info.get("givenname", ""), surname=user_obj.info.get("surname", "")) label = label[0:allowed_label_len] url_label = quote(label.encode("utf-8")) url_issuer = quote(issuer.encode("utf-8")) if hash_algo.lower() != "sha1": hash_algo = "algorithm={0!s}&".format(hash_algo) else: # If the hash_algo is SHA1, we do not add it to the QR code to keep # the QR code simpler hash_algo = "" if tokentype.lower() == "totp": period = "period={0!s}&".format(period) else: period = "" return ("otpauth://{tokentype!s}/{label!s}?secret={secret!s}&" "{counter!s}{hash!s}{period!s}" "digits={digits!s}&" "issuer={issuer!s}{extra}".format(tokentype=tokentype, label=url_label, secret=otpkey, hash=hash_algo, period=period, digits=digits, issuer=url_issuer, counter=counter, extra=_construct_extra_parameters(extra_data)))
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 ``attributes``, which are displayed in the JSON response. """ res = False options = options or {} message = get_action_values_from_options(SCOPE.AUTH, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT sslverify = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1" sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1") attributes = None data = None challenge = b32encode_and_unicode(geturandom()) fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: # We send the challenge to the Firebase service fb_gateway = create_sms_instance(fb_identifier) url = fb_gateway.smsgateway.option_dict.get(FIREBASE_CONFIG.REGISTRATION_URL) message_on_mobile = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT, options) or DEFAULT_MOBILE_TEXT title = get_action_values_from_options(SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA" smartphone_data = {"nonce": challenge, "question": message_on_mobile, "serial": self.token.serial, "title": title, "sslverify": sslverify, "url": url} # Create the signature. # value to string sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**smartphone_data) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend()) # Sign the data with PKCS1 padding. Not all Androids support PSS padding. signature = privkey_obj.sign(sign_string.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) smartphone_data["signature"] = b32encode_and_unicode(signature) res = fb_gateway.submit_message(self.get_tokeninfo("firebase_token"), smartphone_data) if not res: raise ValidateError("Failed to submit message to firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format(self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError("The token has no tokeninfo. Can not send via firebase service.") validity = int(get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() return True, message, db_challenge.transaction_id, attributes
def test_02_api_push_poll(self): r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", FB_CONFIG_VALS) self.assertGreater(r, 0) # create a new push token tokenobj = self._create_push_token() serial = tokenobj.get_serial() # 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.service_account.Credentials' '.from_service_account_file') as mySA: # alternative: side_effect instead of return_value mySA.return_value = _create_credential_mock() # 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. # No push notification is submitted to firebase, but a challenge is created anyway 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 == 400, res) jsonresp = res.json self.assertFalse(jsonresp.get("result").get("status")) self.assertEqual( jsonresp.get("result").get("error").get("code"), 401) self.assertEqual( jsonresp.get("result").get("error").get("message"), "ERR401: Failed to submit message to Firebase service.") # first create a signature ts = datetime.utcnow().isoformat() sign_string = u"{serial}|{timestamp}".format(serial=serial, timestamp=ts) sig = self.smartphone_private_key.sign(sign_string.encode('utf8'), padding.PKCS1v15(), hashes.SHA256()) # now check that we receive the challenge when polling with self.app.test_request_context('/ttype/push', method='GET', data={ "serial": serial, "timestamp": ts, "signature": b32encode(sig) }): res = self.app.full_dispatch_request() # check that the serial was set in flask g (via before_request in ttype.py) self.assertTrue(self.app_context.g.serial, serial) self.assertTrue(res.status_code == 200, res) self.assertTrue(res.json['result']['status']) chall = res.json['result']['value'][0] self.assertTrue(chall) challenge = chall["nonce"] # This is what the smartphone answers. # create the signature: sign_data = "{0!s}|{1!s}".format(challenge, serial) signature = b32encode_and_unicode( self.smartphone_private_key.sign(sign_data.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())) # Answer the challenge with self.app.test_request_context('/ttype/push', method='POST', data={ "serial": 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'])
def test_01_init_token(self): set_policy( name="allow_2step", action=["hotp_2step=allow", "enrollHOTP=1", "delete"], scope=SCOPE.ADMIN, ) with self.app.test_request_context('/token/init', method='POST', data={"type": "hotp", "genkey": "1", "2stepinit": "1"}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = json.loads(res.data.decode('utf8')).get("result") self.assertTrue(result.get("status") is True, result) self.assertTrue(result.get("value") is True, result) detail = json.loads(res.data.decode('utf8')).get("detail") serial = detail.get("serial") otpkey_url = detail.get("otpkey", {}).get("value") server_component = binascii.unhexlify(otpkey_url.split("/")[2]) google_url = detail["googleurl"]["value"] self.assertIn('2step_difficulty=10000', google_url) self.assertIn('2step_salt=8', google_url) self.assertIn('2step_output=20', google_url) self.assertEqual(detail['2step_difficulty'], 10000) self.assertEqual(detail['2step_salt'], 8) self.assertEqual(detail['2step_output'], 20) client_component = b"VRYSECRT" checksum = hashlib.sha1(client_component).digest()[:4] base32check_client_component = b32encode_and_unicode(checksum + client_component).strip("=") # Try to do a 2stepinit on a second step will raise an error with self.app.test_request_context('/token/init', method='POST', data={"type": "hotp", "2stepinit": "1", "serial": serial, "otpkey": base32check_client_component, "otpkeyformat": "base32check"}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 400) result = json.loads(res.data.decode('utf8')).get("result") self.assertIn('2stepinit is only to be used in the first initialization step', result.get("error").get("message")) # Invalid base32check will raise an error with self.app.test_request_context('/token/init', method='POST', data={"type": "hotp", "2stepinit": "1", "serial": serial, "otpkey": "A" + base32check_client_component[1:], "otpkeyformat": "base32check"}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 400) result = json.loads(res.data.decode('utf8')).get("result") self.assertIn('Malformed base32check data: Incorrect checksum', result.get("error").get("message")) # Authentication does not work yet! wrong_otp_value = HmacOtp().generate(key=server_component, counter=1) with self.app.test_request_context('/validate/check', method='POST', data={"serial": serial, "pass": wrong_otp_value}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = json.loads(res.data.decode('utf8')) self.assertTrue(result.get("result").get("status")) self.assertFalse(result.get("result").get("value")) self.assertEqual(result.get("detail").get("message"), u'matching 1 tokens, Token is disabled') # Now doing the correct 2nd step with self.app.test_request_context('/token/init', method='POST', data={"type": "hotp", "serial": serial, "otpkey": base32check_client_component, "otpkeyformat": "base32check"}, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = json.loads(res.data.decode('utf8')).get("result") self.assertTrue(result.get("status") is True, result) self.assertTrue(result.get("value") is True, result) detail = json.loads(res.data.decode('utf8')).get("detail") otpkey_url = detail.get("otpkey", {}).get("value") otpkey = otpkey_url.split("/")[2] self.assertNotIn('2step', detail) # Now try to authenticate otpkey_bin = binascii.unhexlify(otpkey) otp_value = HmacOtp().generate(key=otpkey_bin, counter=1) with self.app.test_request_context('/validate/check', method='POST', data={"serial": serial, "pass": otp_value}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) result = json.loads(res.data.decode('utf8')).get("result") self.assertEqual(result.get("status"), True) self.assertEqual(result.get("value"), True) # Check that the OTP key is what we expected it to be expected_secret = pbkdf2(binascii.hexlify(server_component), client_component, 10000, 20) self.assertEqual(otpkey_bin, expected_secret) with self.app.test_request_context('/token/'+ serial, method='DELETE', headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) delete_policy("allow_2step")
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, ACTION.CHALLENGETEXT, options) or DEFAULT_CHALLENGE_TEXT reply_dict = {} data = None # Initially we assume there is no error from Firebase res = True fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if fb_identifier: challenge = b32encode_and_unicode(geturandom()) if fb_identifier != POLL_ONLY: # We only push to Firebase if this tokens does NOT POLL_ONLY. fb_gateway = create_sms_instance(fb_identifier) registration_url = get_action_values_from_options( SCOPE.ENROLL, PUSH_ACTION.REGISTRATION_URL, options=options) pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER) smartphone_data = _build_smartphone_data( self.token.serial, challenge, registration_url, pem_privkey, options) res = fb_gateway.submit_message( self.get_tokeninfo("firebase_token"), smartphone_data) # Create the challenge in the challenge table if either the message # was successfully submitted to the Firebase API or if polling is # allowed in general or for this specific token. allow_polling = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING, options=options) or PushAllowPolling.ALLOW if ((allow_polling == PushAllowPolling.ALLOW or (allow_polling == PushAllowPolling.TOKEN and is_true( self.get_tokeninfo(POLLING_ALLOWED, default='True')))) or res): validity = int( get_from_config('DefaultChallengeValidityTime', 120)) tokentype = self.get_tokentype().lower() # Maybe there is a PushChallengeValidityTime... lookup_for = tokentype.capitalize() + 'ChallengeValidityTime' validity = int(get_from_config(lookup_for, validity)) # Create the challenge in the database db_challenge = Challenge(self.token.serial, transaction_id=transactionid, challenge=challenge, data=data, session=options.get("session"), validitytime=validity) db_challenge.save() self.challenge_janitor() # If sending the Push message failed, we still raise an error and a warning. if not res: log.warning( u"Failed to submit message to Firebase service for token {0!s}." .format(self.token.serial)) raise ValidateError( "Failed to submit message to Firebase service.") else: log.warning(u"The token {0!s} has no tokeninfo {1!s}. " u"The message could not be sent.".format( self.token.serial, PUSH_ACTION.FIREBASE_CONFIG)) raise ValidateError( "The token has no tokeninfo. Can not send via Firebase service." ) return True, message, db_challenge.transaction_id, reply_dict
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")