def test_05_strip_key(self): stripped_pubkey = strip_key(self.smartphone_public_key_pem) self.assertIn("-BEGIN PUBLIC KEY-", self.smartphone_public_key_pem) self.assertNotIn("-BEGIN PUBLIC KEY_", stripped_pubkey) self.assertNotIn("-", stripped_pubkey) self.assertEqual(strip_key(stripped_pubkey), stripped_pubkey) self.assertEqual(strip_key("\n\n" + stripped_pubkey + "\n\n"), stripped_pubkey)
class PushTokenTestCase(MyTestCase): serial1 = "PUSH00001" # We now allow white spaces in the firebase config name firebase_config_name = "my firebase config" smartphone_private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, backend=default_backend()) smartphone_public_key = smartphone_private_key.public_key() smartphone_public_key_pem = to_unicode( smartphone_public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)) # The smartphone sends the public key in URLsafe and without the ----BEGIN header smartphone_public_key_pem_urlsafe = strip_key( smartphone_public_key_pem).replace("+", "-").replace("/", "_") def test_01_create_token(self): db_token = Token(self.serial1, tokentype="push") db_token.save() token = PushTokenClass(db_token) self.assertEqual(token.token.serial, self.serial1) self.assertEqual(token.token.tokentype, "push") self.assertEqual(token.type, "push") class_prefix = token.get_class_prefix() self.assertEqual(class_prefix, "PIPU") self.assertEqual(token.get_class_type(), "push") # Test to do the 2nd step, although the token is not yet in clientwait self.assertRaises(ParameterError, token.update, { "otpkey": "1234", "pubkey": "1234", "serial": self.serial1 }) # Run enrollment step 1 token.update({"genkey": 1}) # Now the token is in the state clientwait, but insufficient parameters would still fail self.assertRaises(ParameterError, token.update, {"otpkey": "1234"}) self.assertRaises(ParameterError, token.update, { "otpkey": "1234", "pubkey": "1234" }) # Unknown config self.assertRaises(ParameterError, token.get_init_detail, params={"firebase_config": "bla"}) fb_config = { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" } # Wrong JSON file self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Wrong Project number fb_config[FIREBASE_CONFIG.JSON_CONFIG] = FIREBASE_FILE self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) # Missing APP_ID self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" }) # Missing API_KEY_IOS self.assertRaises( ConfigAdminError, set_smsgateway, "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.JSON_CONFIG: CLIENT_FILE, FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.APP_ID_IOS: "1", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "4" }) # Everything is fine fb_config[FIREBASE_CONFIG.PROJECT_ID] = "test-123456" r = set_smsgateway( "fb1", u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", fb_config) self.assertTrue(r > 0) detail = token.get_init_detail( params={"firebase_config": self.firebase_config_name}) self.assertEqual(detail.get("serial"), self.serial1) self.assertEqual(detail.get("rollout_state"), "clientwait") enrollment_credential = detail.get("enrollment_credential") self.assertTrue("pushurl" in detail) self.assertFalse("otpkey" in detail) # Run enrollment step 2 token.update({ "enrollment_credential": enrollment_credential, "serial": self.serial1, "fbtoken": "firebasetoken", "pubkey": self.smartphone_public_key_pem_urlsafe }) self.assertEqual(token.get_tokeninfo("firebase_token"), "firebasetoken") self.assertEqual(token.get_tokeninfo("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertTrue( token.get_tokeninfo("public_key_server").startswith( u"-----BEGIN RSA PUBLIC KEY-----\n"), token.get_tokeninfo("public_key_server")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(token.get_tokeninfo("public_key_server")), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) self.assertTrue( token.get_tokeninfo("private_key_server").startswith( u"-----BEGIN RSA PRIVATE KEY-----\n"), token.get_tokeninfo("private_key_server")) parsed_server_privkey = serialization.load_pem_private_key( to_bytes(token.get_tokeninfo("private_key_server")), None, default_backend()) self.assertIsInstance(parsed_server_privkey, RSAPrivateKey) detail = token.get_init_detail() self.assertEqual(detail.get("rollout_state"), "enrolled") augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_stripped_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertEqual(parsed_server_pubkey.public_numbers(), parsed_stripped_server_pubkey.public_numbers()) remove_token(self.serial1) def test_02_api_enroll(self): self.authenticate() # Failed enrollment due to missing policy with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertNotEqual(res.status_code, 200) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "Missing enrollment policy for push token: push_firebase_configuration" ) self.assertEqual(error.get("code"), 303) r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", { FIREBASE_CONFIG.REGISTRATION_URL: "http://test/ttype/push", FIREBASE_CONFIG.TTL: 10, FIREBASE_CONFIG.API_KEY: "1", FIREBASE_CONFIG.APP_ID: "2", FIREBASE_CONFIG.PROJECT_NUMBER: "3", FIREBASE_CONFIG.PROJECT_ID: "test-123456", FIREBASE_CONFIG.JSON_CONFIG: FIREBASE_FILE }) self.assertTrue(r > 0) set_policy("push1", scope=SCOPE.ENROLL, action="{0!s}={1!s}".format(PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name)) # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step. Failing with wrong serial number with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": "wrongserial", "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 404, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "No token with this serial number in the rollout state 'clientwait'." ) # 2nd step. Fails with missing enrollment credential with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT", "enrollment_credential": "WRonG" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 400, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "ERR905: Invalid enrollment credential. You are not authorized to finalize this token." ) # 2nd step: as performed by the smartphone with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name) @responses.activate def test_03a_api_authenticate_fail(self): # This tests the failed to communicate to the firebase service 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 failing communication (status 500) responses.add( responses.POST, 'https://fcm.googleapis.com/v1/projects/test-123456/messages:send', body="""{}""", status=500, 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 == 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.") # Our ServiceAccountCredentials mock has been called once, because no access token has been fetched before self.assertEqual(len(mySA.from_json_keyfile_name.mock_calls), 1) self.assertIn(FIREBASE_FILE, get_app_local_store()["firebase_token"]) @responses.activate 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 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() @responses.activate 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_05_strip_key(self): stripped_pubkey = strip_key(self.smartphone_public_key_pem) self.assertIn("-BEGIN PUBLIC KEY-", self.smartphone_public_key_pem) self.assertNotIn("-BEGIN PUBLIC KEY_", stripped_pubkey) self.assertNotIn("-", stripped_pubkey) self.assertEqual(strip_key(stripped_pubkey), stripped_pubkey) self.assertEqual(strip_key("\n\n" + stripped_pubkey + "\n\n"), stripped_pubkey) @responses.activate 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")
class TtypePushAPITestCase(MyApiTestCase): """ test /ttype/push """ server_private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) server_private_key_pem = to_unicode( server_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())) server_public_key_pem = to_unicode( server_private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)) # We now allow white spaces in the firebase config name firebase_config_name = "my firebase config" smartphone_private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, backend=default_backend()) smartphone_public_key = smartphone_private_key.public_key() smartphone_public_key_pem = to_unicode( smartphone_public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)) # The smartphone sends the public key in URLsafe and without the ----BEGIN header smartphone_public_key_pem_urlsafe = strip_key( smartphone_public_key_pem).replace("+", "-").replace("/", "_") serial_push = "PIPU001" def _create_push_token(self): tparams = {'type': 'push', 'genkey': 1} tparams.update(FB_CONFIG_VALS) tok = init_token(param=tparams) tok.add_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name) tok.add_tokeninfo(PUBLIC_KEY_SMARTPHONE, self.smartphone_public_key_pem_urlsafe) tok.add_tokeninfo('firebase_token', 'firebaseT') tok.add_tokeninfo(PUBLIC_KEY_SERVER, self.server_public_key_pem) tok.add_tokeninfo(PRIVATE_KEY_SERVER, self.server_private_key_pem, 'password') tok.del_tokeninfo("enrollment_credential") tok.token.rollout_state = "enrolled" tok.token.active = True return tok def test_00_create_realms(self): self.setUp_user_realms() def test_01_api_enroll_push(self): self.authenticate() # Failed enrollment due to missing policy with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertNotEqual(res.status_code, 200) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "Missing enrollment policy for push token: push_firebase_configuration" ) self.assertEqual(error.get("code"), 303) r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", FB_CONFIG_VALS) self.assertTrue(r > 0) set_policy("push1", scope=SCOPE.ENROLL, action="{0!s}={1!s},{2!s}={3!s},{4!s}={5!s}".format( PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name, PUSH_ACTION.REGISTRATION_URL, REGISTRATION_URL, PUSH_ACTION.TTL, TTL)) # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step. Failing with wrong serial number with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": "wrongserial", "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 404, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "No token with this serial number in the rollout state 'clientwait'." ) # 2nd step. Fails with missing enrollment credential with self.app.test_request_context( '/ttype/push', method='POST', data={ "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT", "enrollment_credential": "WRonG" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 400, res) status = res.json.get("result").get("status") self.assertFalse(status) error = res.json.get("result").get("error") self.assertEqual( error.get("message"), "ERR905: Invalid enrollment credential. You are not authorized to finalize this token." ) # 2nd step: as performed by the smartphone with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, rsa.RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name) # remove the token remove_token(serial) 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_03_api_enroll_push_poll_only(self): """Enroll a poll-only push token""" self.authenticate() # Set policy for poll only set_policy("push1", scope=SCOPE.ENROLL, action="{0!s}={1!s},{2!s}={3!s},{4!s}={5!s}".format( PUSH_ACTION.FIREBASE_CONFIG, POLL_ONLY, PUSH_ACTION.REGISTRATION_URL, REGISTRATION_URL, PUSH_ACTION.TTL, TTL)) # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertIn("pushurl", detail) # check that the new URL contains the serial number self.assertIn("&serial=PIPU", detail.get("pushurl").get("value")) # The firebase settings are NOT contained in the QR Code, since we do poll_only # poll_only self.assertNotIn("appid=", detail.get("pushurl").get("value")) self.assertNotIn("appidios=", detail.get("pushurl").get("value")) self.assertNotIn("apikeyios=", detail.get("pushurl").get("value")) self.assertNotIn("otpkey", detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step: as performed by the smartphone. Also in POLL_ONLY the smartphone needs to send # an empty "fbtoken" with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, rsa.RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), POLL_ONLY) # remove the token remove_token(serial) # remove the policy delete_policy("push1")
class PushAPITestCase(MyApiTestCase): """ test the api.validate endpoints """ server_private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) server_private_key_pem = to_unicode( server_private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())) server_public_key_pem = to_unicode( server_private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)) # We now allow white spaces in the firebase config name firebase_config_name = "my firebase config" smartphone_private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, backend=default_backend()) smartphone_public_key = smartphone_private_key.public_key() smartphone_public_key_pem = to_unicode( smartphone_public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)) # The smartphone sends the public key in URLsafe and without the ----BEGIN header smartphone_public_key_pem_urlsafe = strip_key( smartphone_public_key_pem).replace("+", "-").replace("/", "_") serial_push = "PIPU001" def test_00_create_realms(self): self.setUp_user_realms() def test_01_push_token_reorder_list(self): """ * Policy push_wait * The user has two tokens. SPASS and Push with the same PIN. A /validate/check request is sent with this PIN. The PIN could trigger a challenge response with the Push, but since the token list is reordered and the Spass token already successfully authenticates, the push token is not evaluated anymore. """ # set policy set_policy("push1", action="{0!s}=20".format(PUSH_ACTION.WAIT), scope=SCOPE.AUTH) set_policy("push2", scope=SCOPE.ENROLL, action="{0!s}={1!s},{2!s}={3!s},{4!s}={5!s}".format( PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name, PUSH_ACTION.REGISTRATION_URL, REGISTRATION_URL, PUSH_ACTION.TTL, TTL)) # Create push config r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", FB_CONFIG_VALS) self.assertTrue(r > 0) # create push token for user # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "pin": "otppin", "user": "******", "realm": self.realm1, "serial": self.serial_push, "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step: as performed by the smartphone with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name) # create spass token for user init_token({ "serial": "spass01", "type": "spass", "pin": "otppin" }, user=User("selfservice", self.realm1)) # check, if the user has two tokens, now toks = get_tokens(user=User("selfservice", self.realm1)) self.assertEqual(2, len(toks)) self.assertEqual("push", toks[0].type) self.assertEqual("spass", toks[1].type) # authenticate with spass with self.app.test_request_context('/validate/check', method='POST', data={ "user": "******", "realm": self.realm1, "pass": "******" }): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) data = res.json self.assertTrue(data.get("result").get("value")) # successful auth with spass self.assertEqual("spass", data.get("detail").get("type")) remove_token(self.serial_push) remove_token("spass01") delete_policy("push1") delete_policy("push2") def test_02_push_token_do_not_wait_if_disabled(self): """ * Policy push_wait * The user has two tokens. HOTP chal-resp and Push with the same PIN. * But in this case, the push token is disabled. * The user should get the response immediately. A /validate/check request is sent with this PIN. The PIN will only trigger the HOTP, push will not wait, since it is disabled. """ # set policy set_policy("push1", action="{0!s}=20".format(PUSH_ACTION.WAIT), scope=SCOPE.AUTH) set_policy("push2", scope=SCOPE.ENROLL, action="{0!s}={1!s},{2!s}={3!s}".format( PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name, PUSH_ACTION.REGISTRATION_URL, REGISTRATION_URL)) set_policy("chalresp", action="{0!s}=hotp".format(ACTION.CHALLENGERESPONSE), scope=SCOPE.AUTH) # Create push config r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", FB_CONFIG_VALS) self.assertTrue(r > 0) # create push token for user # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "pin": "otppin", "user": "******", "realm": self.realm1, "serial": self.serial_push, "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # 2nd step: as performed by the smartphone with self.app.test_request_context( '/ttype/push', method='POST', data={ "enrollment_credential": enrollment_credential, "serial": serial, "pubkey": self.smartphone_public_key_pem_urlsafe, "fbtoken": "firebaseT" }): res = self.app.full_dispatch_request() self.assertTrue(res.status_code == 200, res) detail = res.json.get("detail") # still the same serial number self.assertEqual(serial, detail.get("serial")) self.assertEqual(detail.get("rollout_state"), "enrolled") # Now the smartphone gets a public key from the server augmented_pubkey = "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----\n".format( detail.get("public_key")) parsed_server_pubkey = serialization.load_pem_public_key( to_bytes(augmented_pubkey), default_backend()) self.assertIsInstance(parsed_server_pubkey, RSAPublicKey) pubkey = detail.get("public_key") # Now check, what is in the token in the database toks = get_tokens(serial=serial) self.assertEqual(len(toks), 1) token_obj = toks[0] self.assertEqual(token_obj.token.rollout_state, u"enrolled") self.assertTrue(token_obj.token.active) tokeninfo = token_obj.get_tokeninfo() self.assertEqual(tokeninfo.get("public_key_smartphone"), self.smartphone_public_key_pem_urlsafe) self.assertEqual(tokeninfo.get("firebase_token"), u"firebaseT") self.assertEqual( tokeninfo.get("public_key_server").strip().strip( "-BEGIN END RSA PUBLIC KEY-").strip(), pubkey) # The token should also contain the firebase config self.assertEqual(tokeninfo.get(PUSH_ACTION.FIREBASE_CONFIG), self.firebase_config_name) # create HOTP token for user init_token( { "serial": "hotp01", "type": "hotp", "pin": "otppin", "otpkey": self.otpkey }, user=User("selfservice", self.realm1)) # disable the push token enable_token(self.serial_push, False) # check, if the user has two tokens, now toks = get_tokens(user=User("selfservice", self.realm1)) self.assertEqual(2, len(toks)) self.assertEqual("push", toks[0].type) self.assertFalse(toks[0].is_active()) self.assertEqual("hotp", toks[1].type) # authenticate with hotp with self.app.test_request_context('/validate/check', method='POST', data={ "user": "******", "realm": self.realm1, "pass": "******" }): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) data = res.json self.assertFalse(data.get("result").get("value")) # This is the auth-request for the HOTP token detail = data.get("detail") multi_challenge = detail.get("multi_challenge") self.assertEqual(multi_challenge[0].get("type"), "hotp") self.assertEqual(multi_challenge[0].get("serial"), "hotp01") self.assertEqual("interactive", multi_challenge[0].get("client_mode")) remove_token(self.serial_push) remove_token("hotp01") delete_policy("push1") delete_policy("push2") delete_policy("chalresp") def test_03_unfinished_enrolled_push_token(self): """ * The user has a push token where the enrollment process was not completed A /validate/check request is sent with this PIN. """ # set policy set_policy("push2", scope=SCOPE.ENROLL, action="{0!s}={1!s},{2!s}={3!s}".format( PUSH_ACTION.FIREBASE_CONFIG, self.firebase_config_name, PUSH_ACTION.REGISTRATION_URL, REGISTRATION_URL)) # Create push config r = set_smsgateway( self.firebase_config_name, u'privacyidea.lib.smsprovider.FirebaseProvider.FirebaseProvider', "myFB", FB_CONFIG_VALS) self.assertTrue(r > 0) # create push token for user # 1st step with self.app.test_request_context('/token/init', method='POST', data={ "type": "push", "pin": "otppin", "user": "******", "realm": self.realm1, "serial": self.serial_push, "genkey": 1 }, headers={'Authorization': self.at}): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) detail = res.json.get("detail") serial = detail.get("serial") self.assertEqual(detail.get("rollout_state"), "clientwait") self.assertTrue("pushurl" in detail) # check that the new URL contains the serial number self.assertTrue( "&serial=PIPU" in detail.get("pushurl").get("value")) self.assertTrue("appid=" in detail.get("pushurl").get("value")) self.assertTrue("appidios=" in detail.get("pushurl").get("value")) self.assertTrue("apikeyios=" in detail.get("pushurl").get("value")) self.assertFalse("otpkey" in detail) enrollment_credential = detail.get("enrollment_credential") # We skip the 2nd step of the enrollment! # But we activate the token on purpose! enable_token(self.serial_push) # authenticate with push with self.app.test_request_context('/validate/check', method='POST', data={ "user": "******", "realm": self.realm1, "pass": "******" }): res = self.app.full_dispatch_request() self.assertEqual(res.status_code, 200) data = res.json self.assertFalse(data.get("result").get("value")) detail = data.get("detail") self.assertEqual(detail.get("message"), "Token is not yet enrolled") remove_token(self.serial_push) delete_policy("push2")