1- from firebase_admin import _apps as firebase_apps
1+ from datetime import datetime
22from firebase_admin import initialize_app
3+ from firebase_admin import get_app
34from firebase_admin .credentials import ApplicationDefault
45from firebase_admin .firestore import client as firestore_client
5- from googleapiclient .http import build_http
6+ from google .auth .exceptions import RefreshError
7+ from google .auth .transport .requests import Request as GoogleAuthRequest
68from google .cloud .firestore import DELETE_FIELD
9+ from google .oauth2 .credentials import Credentials
10+ from json import loads
711from logging import error
812from logging import info
913from logging import warning
10- from oauth2client .client import HttpAccessTokenRefreshError
11- from oauth2client .client import OAuth2Credentials
12- from oauth2client .client import Storage
1314from os import environ
14- from threading import Lock
15+
16+ # The scope to request for the Google Calendar API.
17+ GOOGLE_CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'
18+
19+ # The token endpoint for Google OAuth.
20+ GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
21+
22+ # The timestamp format used by oauth2client credential JSON.
23+ OAUTH2CLIENT_EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
1524
1625
1726class Firestore (object ):
1827 """A wrapper around the Cloud Firestore database."""
1928
2029 def __init__ (self ):
2130 # Only initialize Firebase once.
22- if not len (firebase_apps ):
31+ try :
32+ get_app ()
33+ except ValueError :
2334 initialize_app (ApplicationDefault (), {
2435 'projectId' : environ ['GOOGLE_CLOUD_PROJECT' ]
2536 })
@@ -54,6 +65,37 @@ def google_calendar_secrets(self):
5465
5566 return secrets .to_dict ()
5667
68+ def _google_calendar_credentials_from_json (self , credentials_json ):
69+ """Loads Google Calendar credentials from current or legacy JSON."""
70+
71+ info = loads (credentials_json )
72+ if info .get ('invalid' ):
73+ return None
74+
75+ scopes = info .get ('scopes' ) or [GOOGLE_CALENDAR_SCOPE ]
76+
77+ # Migrate JSON written by the deprecated oauth2client package.
78+ if 'access_token' in info :
79+ return Credentials (
80+ token = info .get ('access_token' ),
81+ refresh_token = info .get ('refresh_token' ),
82+ token_uri = info .get ('token_uri' ) or GOOGLE_TOKEN_URI ,
83+ client_id = info .get ('client_id' ),
84+ client_secret = info .get ('client_secret' ),
85+ scopes = scopes ,
86+ expiry = self ._parse_oauth2client_expiry (
87+ info .get ('token_expiry' )))
88+
89+ return Credentials .from_authorized_user_info (info , scopes = scopes )
90+
91+ def _parse_oauth2client_expiry (self , expiry ):
92+ """Parses oauth2client's naive UTC expiry timestamp."""
93+
94+ if not expiry :
95+ return None
96+
97+ return datetime .strptime (expiry , OAUTH2CLIENT_EXPIRY_FORMAT )
98+
5799 def google_calendar_credentials (self , key ):
58100 """Loads and refreshes Google Calendar API credentials."""
59101
@@ -64,23 +106,31 @@ def google_calendar_credentials(self, key):
64106
65107 # Load the credentials from storage.
66108 try :
67- json = user .get ('google_calendar_credentials' )
109+ credentials_json = user .get ('google_calendar_credentials' )
68110 except KeyError :
69111 warning ('Failed to load Google Calendar credentials.' )
70112 return None
71113
72114 # Use the valid credentials.
73- credentials = OAuth2Credentials .from_json (json )
74- if credentials and not credentials .invalid :
115+ try :
116+ credentials = self ._google_calendar_credentials_from_json (
117+ credentials_json )
118+ except (TypeError , ValueError ) as e :
119+ warning ('Failed to parse Google Calendar credentials: %s' % e )
120+ self .delete_google_calendar_credentials (key )
121+ return None
122+
123+ if credentials and credentials .valid :
75124 return credentials
76125
77126 # Handle invalidation and expiration.
78- if credentials and credentials .access_token_expired :
127+ if credentials and credentials .refresh_token :
79128 try :
80129 info ('Refreshing Google Calendar credentials.' )
81- credentials .refresh (build_http ())
130+ credentials .refresh (GoogleAuthRequest ())
131+ self .update_google_calendar_credentials (key , credentials )
82132 return credentials
83- except HttpAccessTokenRefreshError as e :
133+ except RefreshError as e :
84134 warning ('Google Calendar refresh failed: %s' % e )
85135
86136 # Credentials are missing or refresh failed.
@@ -136,30 +186,31 @@ def update_user(self, key, fields):
136186 user .update (fields )
137187
138188
139- class GoogleCalendarStorage (Storage ):
189+ class GoogleCalendarStorage (object ):
140190 """Credentials storage for the Google Calendar API using Firestore."""
141191
142192 def __init__ (self , key ):
143- super (GoogleCalendarStorage , self ).__init__ (lock = Lock ())
144193 self ._firestore = Firestore ()
145194 self ._key = key
146195
147- def locked_get (self ):
148- """Loads credentials from Firestore and attaches this storage ."""
196+ def get (self ):
197+ """Loads credentials from Firestore."""
149198
150- credentials = self ._firestore .google_calendar_credentials (self ._key )
151- if not credentials :
152- return None
153- credentials .set_store (self )
154- return credentials
199+ return self ._firestore .google_calendar_credentials (self ._key )
155200
156- def locked_put (self , credentials ):
201+ def put (self , credentials ):
157202 """Saves credentials to Firestore."""
158203
159204 self ._firestore .update_google_calendar_credentials (self ._key ,
160205 credentials )
161206
162- def locked_delete (self ):
207+ def refresh (self , credentials ):
208+ """Refreshes credentials and saves the refreshed token."""
209+
210+ credentials .refresh (GoogleAuthRequest ())
211+ self .put (credentials )
212+
213+ def delete (self ):
163214 """Deletes credentials from Firestore."""
164215
165216 self ._firestore .delete_google_calendar_credentials (self ._key )
0 commit comments