We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Hanging at contacting firebase service
When testing the iOS Push token App the communication to the firebase service hangs some times. It is not clear why.

We could however improve the communication here:
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/smsprovider/FirebaseProvider.py#L76

And reuse an existing access_token as long as it has not expired.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `privacyidea/lib/smsprovider/FirebaseProvider.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 #
3 #    2019-02-12 Cornelius Kölbel <cornelius.koelbel@netknights.it>
4 #
5 #
6 #    This program is free software: you can redistribute it and/or
7 #    modify it under the terms of the GNU Affero General Public
8 #    License, version 3, as published by the Free Software Foundation.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU Affero General Public License for more details.
14 #
15 #    You should have received a copy of the
16 #               GNU Affero General Public License
17 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 #
20 
21 __doc__ = """This is the provider class that communicates with Googles
22 Firebase Cloud Messaging Service.
23 This provider is used for the push token and can be used for SMS tokens.
24 """
25 
26 from privacyidea.lib.smsprovider.SMSProvider import (ISMSProvider)
27 from privacyidea.lib.error import ConfigAdminError
28 from privacyidea.lib import _
29 import logging
30 from oauth2client.service_account import ServiceAccountCredentials
31 import requests
32 import json
33 
34 FIREBASE_URL_SEND = 'https://fcm.googleapis.com/v1/projects/{0!s}/messages:send'
35 SCOPES = ['https://www.googleapis.com/auth/cloud-platform',
36           'https://www.googleapis.com/auth/datastore',
37           'https://www.googleapis.com/auth/devstorage.read_write',
38           'https://www.googleapis.com/auth/firebase',
39           'https://www.googleapis.com/auth/identitytoolkit',
40           'https://www.googleapis.com/auth/userinfo.email']
41 
42 log = logging.getLogger(__name__)
43 
44 
45 class FIREBASE_CONFIG:
46     REGISTRATION_URL = "registration URL"
47     TTL = "time to live"
48     JSON_CONFIG = "JSON config file"
49     PROJECT_ID = "projectid"
50     PROJECT_NUMBER = "projectnumber"
51     APP_ID = "appid"
52     API_KEY = "apikey"
53     APP_ID_IOS = "appidios"
54     API_KEY_IOS = "apikeyios"
55 
56 
57 class FirebaseProvider(ISMSProvider):
58 
59     def submit_message(self, firebase_token, data):
60         """
61         send a message to a registered Firebase client
62         This can be a simple OTP value or a cryptographic challenge response.
63 
64         :param firebase_token: The firebase token of the smartphone
65         :type firebase_token: str
66         :param data: the data dictionary part of the message to submit to the phone
67         :type data: dict
68         :return: bool
69         """
70         res = False
71 
72         credentials = ServiceAccountCredentials.\
73                 from_json_keyfile_name(self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG),
74                                        SCOPES)
75 
76         access_token_info = credentials.get_access_token()
77 
78         # Should we do something with expires in?
79         # expires_in = access_token_info.expires_in
80 
81         bearer_token = access_token_info.access_token
82         headers = {
83             'Authorization': u'Bearer {0!s}'.format(bearer_token),
84             'Content-Type': 'application/json; UTF-8',
85         }
86         fcm_message = {
87             "message": {
88                         "data": data,
89                         "token": firebase_token
90                        }
91             }
92 
93         url = FIREBASE_URL_SEND.format(self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID))
94         resp = requests.post(url, data=json.dumps(fcm_message), headers=headers)
95 
96         if resp.status_code == 200:
97             log.debug("Message sent successfully to Firebase service.")
98             res = True
99         else:
100             log.warning(u"Failed to send message to firebase service: {0!s}".format(resp.text))
101 
102         return res
103 
104     def check_configuration(self):
105         """
106         This method checks the sanity of the configuration of this provider.
107         If there is a configuration error, than an exception is raised.
108         :return:
109         """
110         json_file = self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG)
111         server_config = None
112         with open(json_file) as f:
113             server_config = json.load(f)
114         if server_config:
115             if server_config.get("type") != "service_account":
116                 raise ConfigAdminError(description="The JSON file is not a valid firebase credentials file.")
117             project_id = self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID)
118             if server_config.get("project_id") != project_id:
119                 raise ConfigAdminError(description="The project_id you entered does not match the project_id from the JSON file.")
120 
121         else:
122             raise ConfigAdminError(description="Please check your configuration. Can not load JSON file.")
123 
124         # We need at least
125         #         FIREBASE_CONFIG.API_KEY_IOS and FIREBASE_CONFIG.APP_ID_IOS
126         # or
127         #         FIREBASE_CONFIG.API_KEY and FIREBASE_CONFIG.APP_ID
128         android_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID)) and \
129                              bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY))
130         ios_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID_IOS)) and \
131                              bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY_IOS))
132         if not android_configured and not ios_configured:
133             raise ConfigAdminError(description="You need to at least configure either app_id and api_key or"
134                                                " app_id_ios and api_key_ios.")
135 
136     @classmethod
137     def parameters(cls):
138         """
139         Return a dictionary, that describes the parameters and options for the
140         Firebase Provider
141         Parameters are required keys to values.
142 
143         :return: dict
144         """
145         params = {"options_allowed": False,
146                   "parameters": {
147                       FIREBASE_CONFIG.REGISTRATION_URL: {
148                           "required": True,
149                           "description": _('The URL the Push App should contact in the second enrollment step.'
150                                      ' Usually it is the endpoint /ttype/push of the privacyIDEA server.')},
151                       FIREBASE_CONFIG.TTL: {
152                           "required": True,
153                           "description": _('The second enrollment step must be completed within this time (in minutes).')
154                       },
155                       FIREBASE_CONFIG.PROJECT_ID: {
156                           "required": True,
157                           "description": _("The project ID, that the client should use. Get it from your Firebase console.")
158                       },
159                       FIREBASE_CONFIG.PROJECT_NUMBER: {
160                           "required": True,
161                           "description": _(
162                               "The project number, that the client should use. Get it from your Firebase console.")
163                       },
164                       FIREBASE_CONFIG.APP_ID: {
165                           "required": False,
166                           "description": _(
167                               "The App ID, that the Android client should use. Get it from your Firebase console.")
168                       },
169                       FIREBASE_CONFIG.API_KEY: {
170                           "required": False,
171                           "description": _(
172                               "The API Key, that the Android client should use. Get it from your Firebase console.")
173                       },
174                       FIREBASE_CONFIG.APP_ID_IOS:{
175                           "required": False,
176                           "description": _(
177                               "The App ID, that the iOS client should use. Get it from your Firebase console.")
178                       },
179                       FIREBASE_CONFIG.API_KEY_IOS: {
180                           "required": False,
181                           "description": _(
182                               "The API Key, that the iOS client should use. Get it from your Firebase console.")
183                       },
184                       FIREBASE_CONFIG.JSON_CONFIG: {
185                           "required": True,
186                           "description": _("The filename of the JSON config file, that allows privacyIDEA to talk"
187                                            " to the Firebase REST API.")
188                       }
189                   }
190                   }
191         return params
192
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/privacyidea/lib/smsprovider/FirebaseProvider.py b/privacyidea/lib/smsprovider/FirebaseProvider.py
--- a/privacyidea/lib/smsprovider/FirebaseProvider.py
+++ b/privacyidea/lib/smsprovider/FirebaseProvider.py
@@ -25,11 +25,14 @@
 
 from privacyidea.lib.smsprovider.SMSProvider import (ISMSProvider)
 from privacyidea.lib.error import ConfigAdminError
+from privacyidea.lib.framework import get_app_local_store
 from privacyidea.lib import _
 import logging
 from oauth2client.service_account import ServiceAccountCredentials
 import requests
 import json
+import time
+import datetime
 
 FIREBASE_URL_SEND = 'https://fcm.googleapis.com/v1/projects/{0!s}/messages:send'
 SCOPES = ['https://www.googleapis.com/auth/cloud-platform',
@@ -42,6 +45,46 @@
 log = logging.getLogger(__name__)
 
 
+class AccessToken(object):
+
+    def __init__(self, access_token, validity):
+        self.access_token = access_token
+        self.expires_at = time.time() + validity - 10
+
+
+def get_firebase_access_token(config_file_name):
+    """
+    This returns the access token for a given JSON config file name
+
+    :param config_file_name:
+    :return:
+    """
+    fbt = "firebase_token"
+    now = time.time()
+    app_store = get_app_local_store()
+
+    if fbt not in app_store or not isinstance(app_store[fbt], dict):
+        # initialize the firebase_token in the app_store as dict
+        app_store[fbt] = {}
+
+    if not isinstance(app_store[fbt].get(config_file_name), AccessToken) or \
+            now > app_store[fbt].get(config_file_name).expires_at:
+        # If the type of the config is not class AccessToken or
+        # if the token has expired
+        credentials = ServiceAccountCredentials.from_json_keyfile_name(config_file_name, SCOPES)
+        log.debug("Fetching a new access_token for {!r} from firebase...".format(config_file_name))
+        access_token_info = credentials.get_access_token()
+        # Now we set the expiration date for the new access_token with a margin of 10 seconds
+        At = AccessToken(access_token_info.access_token, access_token_info.expires_in)
+        # We do not use a lock here: The worst that could happen is that two threads
+        # fetch new auth tokens concurrently. In this case, one of them wins and is written to the dictionary.
+        app_store[fbt][config_file_name] = At
+        readable_time = datetime.datetime.fromtimestamp(At.expires_at).isoformat()
+        log.debug(u"Setting the expiration for {!r} of the new access_token to {!s}.".format(config_file_name, readable_time))
+
+    return app_store[fbt][config_file_name].access_token
+
+
 class FIREBASE_CONFIG:
     REGISTRATION_URL = "registration URL"
     TTL = "time to live"
@@ -56,6 +99,11 @@
 
 class FirebaseProvider(ISMSProvider):
 
+    def __init__(self, db_smsprovider_object=None, smsgateway=None):
+        ISMSProvider.__init__(self, db_smsprovider_object, smsgateway)
+        self.access_token_info = None
+        self.access_token_expires_at = 0
+
     def submit_message(self, firebase_token, data):
         """
         send a message to a registered Firebase client
@@ -69,16 +117,9 @@
         """
         res = False
 
-        credentials = ServiceAccountCredentials.\
-                from_json_keyfile_name(self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG),
-                                       SCOPES)
-
-        access_token_info = credentials.get_access_token()
-
-        # Should we do something with expires in?
-        # expires_in = access_token_info.expires_in
+        bearer_token = get_firebase_access_token(self.smsgateway.option_dict.get(
+            FIREBASE_CONFIG.JSON_CONFIG))
 
-        bearer_token = access_token_info.access_token
         headers = {
             'Authorization': u'Bearer {0!s}'.format(bearer_token),
             'Content-Type': 'application/json; UTF-8',
@@ -189,4 +230,4 @@
                   }
                   }
         return params
-        
\ No newline at end of file
+

{"golden_diff": "diff --git a/privacyidea/lib/smsprovider/FirebaseProvider.py b/privacyidea/lib/smsprovider/FirebaseProvider.py\n--- a/privacyidea/lib/smsprovider/FirebaseProvider.py\n+++ b/privacyidea/lib/smsprovider/FirebaseProvider.py\n@@ -25,11 +25,14 @@\n \n from privacyidea.lib.smsprovider.SMSProvider import (ISMSProvider)\n from privacyidea.lib.error import ConfigAdminError\n+from privacyidea.lib.framework import get_app_local_store\n from privacyidea.lib import _\n import logging\n from oauth2client.service_account import ServiceAccountCredentials\n import requests\n import json\n+import time\n+import datetime\n \n FIREBASE_URL_SEND = 'https://fcm.googleapis.com/v1/projects/{0!s}/messages:send'\n SCOPES = ['https://www.googleapis.com/auth/cloud-platform',\n@@ -42,6 +45,46 @@\n log = logging.getLogger(__name__)\n \n \n+class AccessToken(object):\n+\n+    def __init__(self, access_token, validity):\n+        self.access_token = access_token\n+        self.expires_at = time.time() + validity - 10\n+\n+\n+def get_firebase_access_token(config_file_name):\n+    \"\"\"\n+    This returns the access token for a given JSON config file name\n+\n+    :param config_file_name:\n+    :return:\n+    \"\"\"\n+    fbt = \"firebase_token\"\n+    now = time.time()\n+    app_store = get_app_local_store()\n+\n+    if fbt not in app_store or not isinstance(app_store[fbt], dict):\n+        # initialize the firebase_token in the app_store as dict\n+        app_store[fbt] = {}\n+\n+    if not isinstance(app_store[fbt].get(config_file_name), AccessToken) or \\\n+            now > app_store[fbt].get(config_file_name).expires_at:\n+        # If the type of the config is not class AccessToken or\n+        # if the token has expired\n+        credentials = ServiceAccountCredentials.from_json_keyfile_name(config_file_name, SCOPES)\n+        log.debug(\"Fetching a new access_token for {!r} from firebase...\".format(config_file_name))\n+        access_token_info = credentials.get_access_token()\n+        # Now we set the expiration date for the new access_token with a margin of 10 seconds\n+        At = AccessToken(access_token_info.access_token, access_token_info.expires_in)\n+        # We do not use a lock here: The worst that could happen is that two threads\n+        # fetch new auth tokens concurrently. In this case, one of them wins and is written to the dictionary.\n+        app_store[fbt][config_file_name] = At\n+        readable_time = datetime.datetime.fromtimestamp(At.expires_at).isoformat()\n+        log.debug(u\"Setting the expiration for {!r} of the new access_token to {!s}.\".format(config_file_name, readable_time))\n+\n+    return app_store[fbt][config_file_name].access_token\n+\n+\n class FIREBASE_CONFIG:\n     REGISTRATION_URL = \"registration URL\"\n     TTL = \"time to live\"\n@@ -56,6 +99,11 @@\n \n class FirebaseProvider(ISMSProvider):\n \n+    def __init__(self, db_smsprovider_object=None, smsgateway=None):\n+        ISMSProvider.__init__(self, db_smsprovider_object, smsgateway)\n+        self.access_token_info = None\n+        self.access_token_expires_at = 0\n+\n     def submit_message(self, firebase_token, data):\n         \"\"\"\n         send a message to a registered Firebase client\n@@ -69,16 +117,9 @@\n         \"\"\"\n         res = False\n \n-        credentials = ServiceAccountCredentials.\\\n-                from_json_keyfile_name(self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG),\n-                                       SCOPES)\n-\n-        access_token_info = credentials.get_access_token()\n-\n-        # Should we do something with expires in?\n-        # expires_in = access_token_info.expires_in\n+        bearer_token = get_firebase_access_token(self.smsgateway.option_dict.get(\n+            FIREBASE_CONFIG.JSON_CONFIG))\n \n-        bearer_token = access_token_info.access_token\n         headers = {\n             'Authorization': u'Bearer {0!s}'.format(bearer_token),\n             'Content-Type': 'application/json; UTF-8',\n@@ -189,4 +230,4 @@\n                   }\n                   }\n         return params\n-        \n\\ No newline at end of file\n+\n", "issue": "Hanging at contacting firebase service\nWhen testing the iOS Push token App the communication to the firebase service hangs some times. It is not clear why.\r\n\r\nWe could however improve the communication here:\r\nhttps://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/smsprovider/FirebaseProvider.py#L76\r\n\r\nAnd reuse an existing access_token as long as it has not expired.\r\n\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#    2019-02-12 Cornelius K\u00f6lbel <cornelius.koelbel@netknights.it>\n#\n#\n#    This program is free software: you can redistribute it and/or\n#    modify it under the terms of the GNU Affero General Public\n#    License, version 3, as published by the Free Software Foundation.\n#\n#    This program is distributed in the hope that it will be useful,\n#    but WITHOUT ANY WARRANTY; without even the implied warranty of\n#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#    GNU Affero General Public License for more details.\n#\n#    You should have received a copy of the\n#               GNU Affero General Public License\n#    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n#\n\n__doc__ = \"\"\"This is the provider class that communicates with Googles\nFirebase Cloud Messaging Service.\nThis provider is used for the push token and can be used for SMS tokens.\n\"\"\"\n\nfrom privacyidea.lib.smsprovider.SMSProvider import (ISMSProvider)\nfrom privacyidea.lib.error import ConfigAdminError\nfrom privacyidea.lib import _\nimport logging\nfrom oauth2client.service_account import ServiceAccountCredentials\nimport requests\nimport json\n\nFIREBASE_URL_SEND = 'https://fcm.googleapis.com/v1/projects/{0!s}/messages:send'\nSCOPES = ['https://www.googleapis.com/auth/cloud-platform',\n          'https://www.googleapis.com/auth/datastore',\n          'https://www.googleapis.com/auth/devstorage.read_write',\n          'https://www.googleapis.com/auth/firebase',\n          'https://www.googleapis.com/auth/identitytoolkit',\n          'https://www.googleapis.com/auth/userinfo.email']\n\nlog = logging.getLogger(__name__)\n\n\nclass FIREBASE_CONFIG:\n    REGISTRATION_URL = \"registration URL\"\n    TTL = \"time to live\"\n    JSON_CONFIG = \"JSON config file\"\n    PROJECT_ID = \"projectid\"\n    PROJECT_NUMBER = \"projectnumber\"\n    APP_ID = \"appid\"\n    API_KEY = \"apikey\"\n    APP_ID_IOS = \"appidios\"\n    API_KEY_IOS = \"apikeyios\"\n\n\nclass FirebaseProvider(ISMSProvider):\n\n    def submit_message(self, firebase_token, data):\n        \"\"\"\n        send a message to a registered Firebase client\n        This can be a simple OTP value or a cryptographic challenge response.\n\n        :param firebase_token: The firebase token of the smartphone\n        :type firebase_token: str\n        :param data: the data dictionary part of the message to submit to the phone\n        :type data: dict\n        :return: bool\n        \"\"\"\n        res = False\n\n        credentials = ServiceAccountCredentials.\\\n                from_json_keyfile_name(self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG),\n                                       SCOPES)\n\n        access_token_info = credentials.get_access_token()\n\n        # Should we do something with expires in?\n        # expires_in = access_token_info.expires_in\n\n        bearer_token = access_token_info.access_token\n        headers = {\n            'Authorization': u'Bearer {0!s}'.format(bearer_token),\n            'Content-Type': 'application/json; UTF-8',\n        }\n        fcm_message = {\n            \"message\": {\n                        \"data\": data,\n                        \"token\": firebase_token\n                       }\n            }\n\n        url = FIREBASE_URL_SEND.format(self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID))\n        resp = requests.post(url, data=json.dumps(fcm_message), headers=headers)\n\n        if resp.status_code == 200:\n            log.debug(\"Message sent successfully to Firebase service.\")\n            res = True\n        else:\n            log.warning(u\"Failed to send message to firebase service: {0!s}\".format(resp.text))\n\n        return res\n\n    def check_configuration(self):\n        \"\"\"\n        This method checks the sanity of the configuration of this provider.\n        If there is a configuration error, than an exception is raised.\n        :return:\n        \"\"\"\n        json_file = self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG)\n        server_config = None\n        with open(json_file) as f:\n            server_config = json.load(f)\n        if server_config:\n            if server_config.get(\"type\") != \"service_account\":\n                raise ConfigAdminError(description=\"The JSON file is not a valid firebase credentials file.\")\n            project_id = self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID)\n            if server_config.get(\"project_id\") != project_id:\n                raise ConfigAdminError(description=\"The project_id you entered does not match the project_id from the JSON file.\")\n\n        else:\n            raise ConfigAdminError(description=\"Please check your configuration. Can not load JSON file.\")\n\n        # We need at least\n        #         FIREBASE_CONFIG.API_KEY_IOS and FIREBASE_CONFIG.APP_ID_IOS\n        # or\n        #         FIREBASE_CONFIG.API_KEY and FIREBASE_CONFIG.APP_ID\n        android_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID)) and \\\n                             bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY))\n        ios_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID_IOS)) and \\\n                             bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY_IOS))\n        if not android_configured and not ios_configured:\n            raise ConfigAdminError(description=\"You need to at least configure either app_id and api_key or\"\n                                               \" app_id_ios and api_key_ios.\")\n\n    @classmethod\n    def parameters(cls):\n        \"\"\"\n        Return a dictionary, that describes the parameters and options for the\n        Firebase Provider\n        Parameters are required keys to values.\n\n        :return: dict\n        \"\"\"\n        params = {\"options_allowed\": False,\n                  \"parameters\": {\n                      FIREBASE_CONFIG.REGISTRATION_URL: {\n                          \"required\": True,\n                          \"description\": _('The URL the Push App should contact in the second enrollment step.'\n                                     ' Usually it is the endpoint /ttype/push of the privacyIDEA server.')},\n                      FIREBASE_CONFIG.TTL: {\n                          \"required\": True,\n                          \"description\": _('The second enrollment step must be completed within this time (in minutes).')\n                      },\n                      FIREBASE_CONFIG.PROJECT_ID: {\n                          \"required\": True,\n                          \"description\": _(\"The project ID, that the client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.PROJECT_NUMBER: {\n                          \"required\": True,\n                          \"description\": _(\n                              \"The project number, that the client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.APP_ID: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The App ID, that the Android client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.API_KEY: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The API Key, that the Android client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.APP_ID_IOS:{\n                          \"required\": False,\n                          \"description\": _(\n                              \"The App ID, that the iOS client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.API_KEY_IOS: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The API Key, that the iOS client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.JSON_CONFIG: {\n                          \"required\": True,\n                          \"description\": _(\"The filename of the JSON config file, that allows privacyIDEA to talk\"\n                                           \" to the Firebase REST API.\")\n                      }\n                  }\n                  }\n        return params\n        ", "path": "privacyidea/lib/smsprovider/FirebaseProvider.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#    2019-02-12 Cornelius K\u00f6lbel <cornelius.koelbel@netknights.it>\n#\n#\n#    This program is free software: you can redistribute it and/or\n#    modify it under the terms of the GNU Affero General Public\n#    License, version 3, as published by the Free Software Foundation.\n#\n#    This program is distributed in the hope that it will be useful,\n#    but WITHOUT ANY WARRANTY; without even the implied warranty of\n#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#    GNU Affero General Public License for more details.\n#\n#    You should have received a copy of the\n#               GNU Affero General Public License\n#    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n#\n\n__doc__ = \"\"\"This is the provider class that communicates with Googles\nFirebase Cloud Messaging Service.\nThis provider is used for the push token and can be used for SMS tokens.\n\"\"\"\n\nfrom privacyidea.lib.smsprovider.SMSProvider import (ISMSProvider)\nfrom privacyidea.lib.error import ConfigAdminError\nfrom privacyidea.lib.framework import get_app_local_store\nfrom privacyidea.lib import _\nimport logging\nfrom oauth2client.service_account import ServiceAccountCredentials\nimport requests\nimport json\nimport time\nimport datetime\n\nFIREBASE_URL_SEND = 'https://fcm.googleapis.com/v1/projects/{0!s}/messages:send'\nSCOPES = ['https://www.googleapis.com/auth/cloud-platform',\n          'https://www.googleapis.com/auth/datastore',\n          'https://www.googleapis.com/auth/devstorage.read_write',\n          'https://www.googleapis.com/auth/firebase',\n          'https://www.googleapis.com/auth/identitytoolkit',\n          'https://www.googleapis.com/auth/userinfo.email']\n\nlog = logging.getLogger(__name__)\n\n\nclass AccessToken(object):\n\n    def __init__(self, access_token, validity):\n        self.access_token = access_token\n        self.expires_at = time.time() + validity - 10\n\n\ndef get_firebase_access_token(config_file_name):\n    \"\"\"\n    This returns the access token for a given JSON config file name\n\n    :param config_file_name:\n    :return:\n    \"\"\"\n    fbt = \"firebase_token\"\n    now = time.time()\n    app_store = get_app_local_store()\n\n    if fbt not in app_store or not isinstance(app_store[fbt], dict):\n        # initialize the firebase_token in the app_store as dict\n        app_store[fbt] = {}\n\n    if not isinstance(app_store[fbt].get(config_file_name), AccessToken) or \\\n            now > app_store[fbt].get(config_file_name).expires_at:\n        # If the type of the config is not class AccessToken or\n        # if the token has expired\n        credentials = ServiceAccountCredentials.from_json_keyfile_name(config_file_name, SCOPES)\n        log.debug(\"Fetching a new access_token for {!r} from firebase...\".format(config_file_name))\n        access_token_info = credentials.get_access_token()\n        # Now we set the expiration date for the new access_token with a margin of 10 seconds\n        At = AccessToken(access_token_info.access_token, access_token_info.expires_in)\n        # We do not use a lock here: The worst that could happen is that two threads\n        # fetch new auth tokens concurrently. In this case, one of them wins and is written to the dictionary.\n        app_store[fbt][config_file_name] = At\n        readable_time = datetime.datetime.fromtimestamp(At.expires_at).isoformat()\n        log.debug(u\"Setting the expiration for {!r} of the new access_token to {!s}.\".format(config_file_name, readable_time))\n\n    return app_store[fbt][config_file_name].access_token\n\n\nclass FIREBASE_CONFIG:\n    REGISTRATION_URL = \"registration URL\"\n    TTL = \"time to live\"\n    JSON_CONFIG = \"JSON config file\"\n    PROJECT_ID = \"projectid\"\n    PROJECT_NUMBER = \"projectnumber\"\n    APP_ID = \"appid\"\n    API_KEY = \"apikey\"\n    APP_ID_IOS = \"appidios\"\n    API_KEY_IOS = \"apikeyios\"\n\n\nclass FirebaseProvider(ISMSProvider):\n\n    def __init__(self, db_smsprovider_object=None, smsgateway=None):\n        ISMSProvider.__init__(self, db_smsprovider_object, smsgateway)\n        self.access_token_info = None\n        self.access_token_expires_at = 0\n\n    def submit_message(self, firebase_token, data):\n        \"\"\"\n        send a message to a registered Firebase client\n        This can be a simple OTP value or a cryptographic challenge response.\n\n        :param firebase_token: The firebase token of the smartphone\n        :type firebase_token: str\n        :param data: the data dictionary part of the message to submit to the phone\n        :type data: dict\n        :return: bool\n        \"\"\"\n        res = False\n\n        bearer_token = get_firebase_access_token(self.smsgateway.option_dict.get(\n            FIREBASE_CONFIG.JSON_CONFIG))\n\n        headers = {\n            'Authorization': u'Bearer {0!s}'.format(bearer_token),\n            'Content-Type': 'application/json; UTF-8',\n        }\n        fcm_message = {\n            \"message\": {\n                        \"data\": data,\n                        \"token\": firebase_token\n                       }\n            }\n\n        url = FIREBASE_URL_SEND.format(self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID))\n        resp = requests.post(url, data=json.dumps(fcm_message), headers=headers)\n\n        if resp.status_code == 200:\n            log.debug(\"Message sent successfully to Firebase service.\")\n            res = True\n        else:\n            log.warning(u\"Failed to send message to firebase service: {0!s}\".format(resp.text))\n\n        return res\n\n    def check_configuration(self):\n        \"\"\"\n        This method checks the sanity of the configuration of this provider.\n        If there is a configuration error, than an exception is raised.\n        :return:\n        \"\"\"\n        json_file = self.smsgateway.option_dict.get(FIREBASE_CONFIG.JSON_CONFIG)\n        server_config = None\n        with open(json_file) as f:\n            server_config = json.load(f)\n        if server_config:\n            if server_config.get(\"type\") != \"service_account\":\n                raise ConfigAdminError(description=\"The JSON file is not a valid firebase credentials file.\")\n            project_id = self.smsgateway.option_dict.get(FIREBASE_CONFIG.PROJECT_ID)\n            if server_config.get(\"project_id\") != project_id:\n                raise ConfigAdminError(description=\"The project_id you entered does not match the project_id from the JSON file.\")\n\n        else:\n            raise ConfigAdminError(description=\"Please check your configuration. Can not load JSON file.\")\n\n        # We need at least\n        #         FIREBASE_CONFIG.API_KEY_IOS and FIREBASE_CONFIG.APP_ID_IOS\n        # or\n        #         FIREBASE_CONFIG.API_KEY and FIREBASE_CONFIG.APP_ID\n        android_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID)) and \\\n                             bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY))\n        ios_configured = bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.APP_ID_IOS)) and \\\n                             bool(self.smsgateway.option_dict.get(FIREBASE_CONFIG.API_KEY_IOS))\n        if not android_configured and not ios_configured:\n            raise ConfigAdminError(description=\"You need to at least configure either app_id and api_key or\"\n                                               \" app_id_ios and api_key_ios.\")\n\n    @classmethod\n    def parameters(cls):\n        \"\"\"\n        Return a dictionary, that describes the parameters and options for the\n        Firebase Provider\n        Parameters are required keys to values.\n\n        :return: dict\n        \"\"\"\n        params = {\"options_allowed\": False,\n                  \"parameters\": {\n                      FIREBASE_CONFIG.REGISTRATION_URL: {\n                          \"required\": True,\n                          \"description\": _('The URL the Push App should contact in the second enrollment step.'\n                                     ' Usually it is the endpoint /ttype/push of the privacyIDEA server.')},\n                      FIREBASE_CONFIG.TTL: {\n                          \"required\": True,\n                          \"description\": _('The second enrollment step must be completed within this time (in minutes).')\n                      },\n                      FIREBASE_CONFIG.PROJECT_ID: {\n                          \"required\": True,\n                          \"description\": _(\"The project ID, that the client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.PROJECT_NUMBER: {\n                          \"required\": True,\n                          \"description\": _(\n                              \"The project number, that the client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.APP_ID: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The App ID, that the Android client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.API_KEY: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The API Key, that the Android client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.APP_ID_IOS:{\n                          \"required\": False,\n                          \"description\": _(\n                              \"The App ID, that the iOS client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.API_KEY_IOS: {\n                          \"required\": False,\n                          \"description\": _(\n                              \"The API Key, that the iOS client should use. Get it from your Firebase console.\")\n                      },\n                      FIREBASE_CONFIG.JSON_CONFIG: {\n                          \"required\": True,\n                          \"description\": _(\"The filename of the JSON config file, that allows privacyIDEA to talk\"\n                                           \" to the Firebase REST API.\")\n                      }\n                  }\n                  }\n        return params\n        \n", "path": "privacyidea/lib/smsprovider/FirebaseProvider.py"}]}