Skip to content

Commit b29ddb6

Browse files
authored
Migrating the Python Admin SDK to google-auth Library (#19)
* Migrating to google-auth library * Added google-auth and requests as dependencies * Updated doc; removed unused import * Adding project_id member to app default credential * Improved error handling logic when constructing credentials
1 parent 721cfd2 commit b29ddb6

10 files changed

+163
-300
lines changed

firebase_admin/auth.py

+18-18
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,18 @@
2222
import threading
2323
import time
2424

25-
from oauth2client import crypt
25+
from google.auth import jwt
26+
from google.auth.transport import requests
27+
import google.oauth2.id_token
2628
import six
2729

2830
import firebase_admin
2931
from firebase_admin import credentials
30-
from firebase_admin import jwt
3132

3233
_auth_lock = threading.Lock()
3334

34-
"""Provided for overriding during tests. (OAuth2 client uses a caching-enabled
35-
HTTP client internally if none provided)
36-
"""
37-
_http = None
35+
"""Provided for overriding during tests."""
36+
_request = requests.Request()
3837

3938
_AUTH_ATTRIBUTE = '_auth'
4039
GCLOUD_PROJECT_ENV_VAR = 'GCLOUD_PROJECT'
@@ -196,7 +195,7 @@ def create_custom_token(self, uid, developer_claims=None):
196195
if developer_claims is not None:
197196
payload['claims'] = developer_claims
198197

199-
return jwt.encode(payload, self._app.credential.signer)
198+
return jwt.encode(self._app.credential.signer, payload)
200199

201200
def verify_id_token(self, id_token):
202201
"""Verifies the signature and data for the provided JWT.
@@ -226,16 +225,19 @@ def verify_id_token(self, id_token):
226225

227226
try:
228227
project_id = self._app.credential.project_id
228+
if project_id is None:
229+
project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR)
229230
except AttributeError:
230231
project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR)
231232

232233
if not project_id:
233-
raise ValueError('Must initialize app with a credentials.Certificate '
234-
'or set your Firebase project ID as the '
235-
'GCLOUD_PROJECT environment variable to call '
236-
'verify_id_token().')
234+
raise ValueError('Failed to ascertain project ID from the credential or the '
235+
'environment. Must initialize app with a credentials.Certificate or '
236+
'set your Firebase project ID as the GCLOUD_PROJECT environment '
237+
'variable to call verify_id_token().')
237238

238-
header, payload = jwt.decode(id_token)
239+
header = jwt.decode_header(id_token)
240+
payload = jwt.decode(id_token, verify=False)
239241
issuer = payload.get('iss')
240242
audience = payload.get('aud')
241243
subject = payload.get('sub')
@@ -286,13 +288,11 @@ def verify_id_token(self, id_token):
286288
'characters. ') + verify_id_token_msg
287289

288290
if error_message:
289-
raise crypt.AppIdentityError(error_message)
291+
raise ValueError(error_message)
290292

291-
verified_claims = jwt.verify_id_token(
293+
verified_claims = google.oauth2.id_token.verify_firebase_token(
292294
id_token,
293-
self.FIREBASE_CERT_URI,
294-
audience=project_id,
295-
kid=header.get('kid'),
296-
http=_http)
295+
request=_request,
296+
audience=project_id)
297297
verified_claims['uid'] = verified_claims['sub']
298298
return verified_claims

firebase_admin/credentials.py

+53-42
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@
1313
# limitations under the License.
1414

1515
"""Firebase credentials module."""
16+
import collections
1617
import json
1718

18-
import httplib2
19+
import google.auth
20+
from google.auth.transport import requests
21+
from google.oauth2 import credentials
22+
from google.oauth2 import service_account
1923

20-
from oauth2client import client
21-
from oauth2client import crypt
2224

25+
_request = requests.Request()
2326

24-
_http = httplib2.Http()
27+
28+
AccessTokenInfo = collections.namedtuple(
29+
'AccessTokenInfo', ['access_token', 'expiry'])
2530

2631

2732
class Base(object):
@@ -39,6 +44,8 @@ def get_credential(self):
3944
class Certificate(Base):
4045
"""A credential initialized from a JSON certificate keyfile."""
4146

47+
_CREDENTIAL_TYPE = 'service_account'
48+
4249
def __init__(self, file_path):
4350
"""Initializes a credential from a certificate file.
4451
@@ -53,23 +60,15 @@ def __init__(self, file_path):
5360
ValueError: If the certificate file is invalid.
5461
"""
5562
super(Certificate, self).__init__()
56-
# TODO(hkj): Clean this up once we are able to take a dependency
57-
# TODO(hkj): on latest oauth2client.
5863
with open(file_path) as json_keyfile:
5964
json_data = json.load(json_keyfile)
60-
if json_data.get('type') != client.SERVICE_ACCOUNT:
61-
raise ValueError('Invalid certificate file. File must contain a '
62-
'"type" field set to "{0}".'.format(client.SERVICE_ACCOUNT))
65+
if json_data.get('type') != self._CREDENTIAL_TYPE:
66+
raise ValueError('Invalid certificate file: "{0}". File must contain a '
67+
'"type" field set to "{1}".'.format(file_path, self._CREDENTIAL_TYPE))
6368
self._project_id = json_data.get('project_id')
64-
self._service_account_email = json_data.get('client_email')
65-
try:
66-
self._signer = crypt.Signer.from_string(json_data.get('private_key'))
67-
except Exception as error:
68-
raise ValueError('Failed to parse the private key string or initialize an '
69-
'RSA signer. Caused by: "{0}".'.format(error))
7069
try:
71-
self._g_credential = client.GoogleCredentials.from_stream(file_path)
72-
except client.ApplicationDefaultCredentialsError as error:
70+
self._g_credential = service_account.Credentials.from_service_account_info(json_data)
71+
except ValueError as error:
7372
raise ValueError('Failed to initialize a certificate credential from file "{0}". '
7473
'Caused by: "{1}"'.format(file_path, error))
7574

@@ -79,25 +78,26 @@ def project_id(self):
7978

8079
@property
8180
def signer(self):
82-
return self._signer
81+
return self._g_credential.signer
8382

8483
@property
8584
def service_account_email(self):
86-
return self._service_account_email
85+
return self._g_credential.service_account_email
8786

8887
def get_access_token(self):
8988
"""Fetches a Google OAuth2 access token using this certificate credential.
9089
9190
Returns:
92-
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
91+
AccessTokenInfo: An access token obtained using the credential.
9392
"""
94-
return self._g_credential.get_access_token(_http)
93+
self._g_credential.refresh(_request)
94+
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)
9595

9696
def get_credential(self):
9797
"""Returns the underlying Google credential.
9898
9999
Returns:
100-
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
100+
google.auth.credentials.Credentials: A Google Auth credential instance."""
101101
return self._g_credential
102102

103103

@@ -108,31 +108,38 @@ def __init__(self):
108108
"""Initializes the Application Default credentials for the current environment.
109109
110110
Raises:
111-
oauth2client.client.ApplicationDefaultCredentialsError: If Application Default
111+
google.auth.exceptions.DefaultCredentialsError: If Application Default
112112
credentials cannot be initialized in the current environment.
113113
"""
114114
super(ApplicationDefault, self).__init__()
115-
self._g_credential = client.GoogleCredentials.get_application_default()
115+
self._g_credential, self._project_id = google.auth.default()
116116

117117
def get_access_token(self):
118118
"""Fetches a Google OAuth2 access token using this application default credential.
119119
120120
Returns:
121-
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
121+
AccessTokenInfo: An access token obtained using the credential.
122122
"""
123-
return self._g_credential.get_access_token(_http)
123+
self._g_credential.refresh(_request)
124+
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)
124125

125126
def get_credential(self):
126127
"""Returns the underlying Google credential.
127128
128129
Returns:
129-
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
130+
google.auth.credentials.Credentials: A Google Auth credential instance."""
130131
return self._g_credential
131132

133+
@property
134+
def project_id(self):
135+
return self._project_id
136+
132137

133138
class RefreshToken(Base):
134139
"""A credential initialized from an existing refresh token."""
135140

141+
_CREDENTIAL_TYPE = 'authorized_user'
142+
136143
def __init__(self, file_path):
137144
"""Initializes a refresh token credential from the specified JSON file.
138145
@@ -146,41 +153,45 @@ def __init__(self, file_path):
146153
super(RefreshToken, self).__init__()
147154
with open(file_path) as json_keyfile:
148155
json_data = json.load(json_keyfile)
149-
if json_data.get('type') != client.AUTHORIZED_USER:
150-
raise ValueError('Invalid refresh token file. File must contain a '
151-
'"type" field set to "{0}".'.format(client.AUTHORIZED_USER))
152-
self._client_id = json_data.get('client_id')
153-
self._client_secret = json_data.get('client_secret')
154-
self._refresh_token = json_data.get('refresh_token')
156+
if json_data.get('type') != self._CREDENTIAL_TYPE:
157+
raise ValueError('Invalid refresh token file: "{0}". File must contain a '
158+
'"type" field set to "{1}".'.format(file_path, self._CREDENTIAL_TYPE))
155159
try:
156-
self._g_credential = client.GoogleCredentials.from_stream(file_path)
157-
except client.ApplicationDefaultCredentialsError as error:
160+
client_id = json_data['client_id']
161+
client_secret = json_data['client_secret']
162+
refresh_token = json_data['refresh_token']
163+
except KeyError as error:
158164
raise ValueError('Failed to initialize a refresh token credential from file "{0}". '
159-
'Caused by: "{1}".'.format(file_path, error))
165+
'Caused by: "{1}"'.format(file_path, error))
166+
self._g_credential = credentials.Credentials(
167+
token=None, refresh_token=refresh_token,
168+
token_uri='https://accounts.google.com/o/oauth2/token',
169+
client_id=client_id, client_secret=client_secret)
160170

161171
@property
162172
def client_id(self):
163-
return self._client_id
173+
return self._g_credential.client_id
164174

165175
@property
166176
def client_secret(self):
167-
return self._client_secret
177+
return self._g_credential.client_secret
168178

169179
@property
170180
def refresh_token(self):
171-
return self._refresh_token
181+
return self._g_credential.refresh_token
172182

173183
def get_access_token(self):
174184
"""Fetches a Google OAuth2 access token using this refresh token credential.
175185
176186
Returns:
177-
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
187+
AccessTokenInfo: An access token obtained using the credential.
178188
"""
179-
return self._g_credential.get_access_token(_http)
189+
self._g_credential.refresh(_request)
190+
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)
180191

181192
def get_credential(self):
182193
"""Returns the underlying Google credential.
183194
184195
Returns:
185-
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
196+
google.auth.credentials.Credentials: A Google Auth credential instance."""
186197
return self._g_credential

0 commit comments

Comments
 (0)