Esempio n. 1
0
 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)
 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")
Esempio n. 4
0
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")