Skip to content

Commit f539137

Browse files
authored
feat(credentials): Add async credentials. (#629)
* feat(credentials): Add async credentials. * fix: Added to required modules libraries. * fix: Imported correct instance of typing.List. * fix: Added method in subclass to override abstract method in class. * fix: Added code font for literal references. * fix: lint * fix: update aiohttp version requirement to allow future versions.
1 parent 6d826fd commit f539137

File tree

3 files changed

+114
-39
lines changed

3 files changed

+114
-39
lines changed

firebase_admin/credentials.py

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,25 @@
1717
import json
1818
import pathlib
1919

20-
import google.auth
21-
from google.auth.transport import requests
22-
from google.oauth2 import credentials
20+
from typing import (
21+
Type,
22+
List
23+
)
24+
25+
import google.auth # type: ignore
26+
from google.auth import default
27+
from google.auth._default_async import default_async # type: ignore
28+
from google.auth.transport import requests # type: ignore
29+
from google.auth.transport import _aiohttp_requests as aiohttp_requests
30+
from google.oauth2 import credentials # type: ignore
31+
from google.oauth2 import _credentials_async as credentials_async
2332
from google.oauth2 import service_account
33+
from google.oauth2 import _service_account_async as service_account_async
2434

2535

26-
_request = requests.Request()
27-
_scopes = [
36+
_request: requests.Request = requests.Request()
37+
_request_async: aiohttp_requests.Request = aiohttp_requests.Request()
38+
_scopes: List[str] = [
2839
'https://www.googleapis.com/auth/cloud-platform',
2940
'https://www.googleapis.com/auth/datastore',
3041
'https://www.googleapis.com/auth/devstorage.read_write',
@@ -33,7 +44,7 @@
3344
'https://www.googleapis.com/auth/userinfo.email'
3445
]
3546

36-
AccessTokenInfo = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
47+
AccessTokenInfo: Type[tuple] = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
3748
"""Data included in an OAuth2 access token.
3849
3950
Contains the access token string and the expiry time. The expirty time is exposed as a
@@ -44,8 +55,8 @@
4455
class Base:
4556
"""Provides OAuth2 access tokens for accessing Firebase services."""
4657

47-
def get_access_token(self):
48-
"""Fetches a Google OAuth2 access token using this credential instance.
58+
def get_access_token(self) -> tuple:
59+
"""Fetches a Google OAuth2 access token using the synchronous credential instance.
4960
5061
Returns:
5162
AccessTokenInfo: An access token obtained using the credential.
@@ -54,8 +65,22 @@ def get_access_token(self):
5465
google_cred.refresh(_request)
5566
return AccessTokenInfo(google_cred.token, google_cred.expiry)
5667

68+
async def get_access_token_async(self) -> tuple:
69+
"""Fetches a Google OAuth2 access token using the asynchronous credential instance.
70+
71+
Returns:
72+
AccessTokenInfo: An access token obtained using the credential.
73+
"""
74+
google_cred = self.get_credential_async()
75+
await google_cred.refresh(_request_async)
76+
return AccessTokenInfo(google_cred.token, google_cred.expiry)
77+
5778
def get_credential(self):
58-
"""Returns the Google credential instance used for authentication."""
79+
"""Returns the Google synchronous credential instance used for authentication."""
80+
raise NotImplementedError
81+
82+
def get_credential_async(self):
83+
"""Returns the Google asynchronous credential instance used for authentication."""
5984
raise NotImplementedError
6085

6186

@@ -64,8 +89,8 @@ class Certificate(Base):
6489

6590
_CREDENTIAL_TYPE = 'service_account'
6691

67-
def __init__(self, cert):
68-
"""Initializes a credential from a Google service account certificate.
92+
def __init__(self, cert: str) -> None:
93+
"""Initializes credentials from a Google service account certificate.
6994
7095
Service account certificates can be downloaded as JSON files from the Firebase console.
7196
To instantiate a credential from a certificate file, either specify the file path or a
@@ -95,44 +120,55 @@ def __init__(self, cert):
95120
try:
96121
self._g_credential = service_account.Credentials.from_service_account_info(
97122
json_data, scopes=_scopes)
123+
self._g_credential_async = service_account_async.Credentials.from_service_account_info(
124+
json_data, scopes=_scopes)
98125
except ValueError as error:
99126
raise ValueError('Failed to initialize a certificate credential. '
100127
'Caused by: "{0}"'.format(error))
101128

102129
@property
103-
def project_id(self):
130+
def project_id(self) -> str:
104131
return self._g_credential.project_id
105132

106133
@property
107-
def signer(self):
134+
def signer(self) -> google.auth.crypt.Signer:
108135
return self._g_credential.signer
109136

110137
@property
111-
def service_account_email(self):
138+
def service_account_email(self) -> str:
112139
return self._g_credential.service_account_email
113140

114-
def get_credential(self):
115-
"""Returns the underlying Google credential.
141+
def get_credential(self) -> service_account.Credentials:
142+
"""Returns the underlying Google synchronous credential.
116143
117144
Returns:
118-
google.auth.credentials.Credentials: A Google Auth credential instance."""
145+
google.auth.credentials.Credentials: A Google Auth synchronous credential instance."""
119146
return self._g_credential
120147

148+
def get_credential_async(self) -> service_account_async.Credentials:
149+
"""Returns the underlying Google asynchronous credential.
150+
151+
Returns:
152+
google.auth._credentials_async.Credentials: A Google Auth asynchronous credential
153+
instance."""
154+
return self._g_credential_async
121155

122156
class ApplicationDefault(Base):
123157
"""A Google Application Default credential."""
124158

125-
def __init__(self):
159+
def __init__(self) -> None:
126160
"""Creates an instance that will use Application Default credentials.
127161
128-
The credentials will be lazily initialized when get_credential() or
129-
project_id() is called. See those methods for possible errors raised.
162+
The credentials will be lazily initialized when ``get_credential()``,
163+
``get_credential_async()`` or ``project_id()`` is called. See those methods for possible
164+
errors raised.
130165
"""
131166
super(ApplicationDefault, self).__init__()
132167
self._g_credential = None # Will be lazily-loaded via _load_credential().
168+
self._g_credential_async = None # Will be lazily-loaded via _load_credential_async().
133169

134-
def get_credential(self):
135-
"""Returns the underlying Google credential.
170+
def get_credential(self) -> credentials.Credentials:
171+
"""Returns the underlying Google synchronous credential.
136172
137173
Raises:
138174
google.auth.exceptions.DefaultCredentialsError: If Application Default
@@ -142,9 +178,20 @@ def get_credential(self):
142178
self._load_credential()
143179
return self._g_credential
144180

181+
def get_credential_async(self) -> credentials_async.Credentials:
182+
"""Returns the underlying Google asynchronous credential.
183+
184+
Raises:
185+
google.auth.exceptions.DefaultCredentialsError: If Application Default
186+
credentials cannot be initialized in the current environment.
187+
Returns:
188+
google.auth._credentials_async.Credentials: A Google Auth credential instance."""
189+
self._load_credential_async()
190+
return self._g_credential_async
191+
145192
@property
146-
def project_id(self):
147-
"""Returns the project_id from the underlying Google credential.
193+
def project_id(self) -> str:
194+
"""Returns the project_id from the underlying Google credentials.
148195
149196
Raises:
150197
google.auth.exceptions.DefaultCredentialsError: If Application Default
@@ -154,21 +201,25 @@ def project_id(self):
154201
self._load_credential()
155202
return self._project_id
156203

157-
def _load_credential(self):
204+
def _load_credential(self) -> None:
158205
if not self._g_credential:
159-
self._g_credential, self._project_id = google.auth.default(scopes=_scopes)
206+
self._g_credential, self._project_id = default(scopes=_scopes)
207+
208+
def _load_credential_async(self) -> None:
209+
if not self._g_credential_async:
210+
self._g_credential_async, self._project_id = default_async(scopes=_scopes)
160211

161212
class RefreshToken(Base):
162-
"""A credential initialized from an existing refresh token."""
213+
"""Credentials initialized from an existing refresh token."""
163214

164215
_CREDENTIAL_TYPE = 'authorized_user'
165216

166-
def __init__(self, refresh_token):
167-
"""Initializes a credential from a refresh token JSON file.
217+
def __init__(self, refresh_token: str) -> None:
218+
"""Initializes credentials from a refresh token JSON file.
168219
169-
The JSON must consist of client_id, client_secret and refresh_token fields. Refresh
170-
token files are typically created and managed by the gcloud SDK. To instantiate
171-
a credential from a refresh token file, either specify the file path or a dict
220+
The JSON must consist of ``client_id``, ``client_secret`` and ``refresh_token`` fields.
221+
Refresh token files are typically created and managed by the gcloud SDK. To instantiate
222+
credentials from a refresh token file, either specify the file path or a dict
172223
representing the parsed contents of the file.
173224
174225
Args:
@@ -194,28 +245,40 @@ def __init__(self, refresh_token):
194245
raise ValueError('Invalid refresh token configuration. JSON must contain a '
195246
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
196247
self._g_credential = credentials.Credentials.from_authorized_user_info(json_data, _scopes)
248+
self._g_credential_async = credentials_async.Credentials.from_authorized_user_info(
249+
json_data,
250+
_scopes
251+
)
197252

198253
@property
199-
def client_id(self):
254+
def client_id(self) -> str:
200255
return self._g_credential.client_id
201256

202257
@property
203-
def client_secret(self):
258+
def client_secret(self) -> str:
204259
return self._g_credential.client_secret
205260

206261
@property
207-
def refresh_token(self):
262+
def refresh_token(self) -> str:
208263
return self._g_credential.refresh_token
209264

210-
def get_credential(self):
211-
"""Returns the underlying Google credential.
265+
def get_credential(self) -> credentials.Credentials:
266+
"""Returns the underlying Google synchronous credential.
212267
213268
Returns:
214-
google.auth.credentials.Credentials: A Google Auth credential instance."""
269+
google.auth.credentials.Credentials: A Google Auth synchronous credential instance."""
215270
return self._g_credential
216271

272+
def get_credential_async(self) -> credentials_async.Credentials:
273+
"""Returns the underlying Google asynchronous credential.
274+
275+
Returns:
276+
google.auth._credentials_async.Credentials: A Google Auth asynchronous credential
277+
instance."""
278+
return self._g_credential_async
279+
217280

218-
def _is_file_path(path):
281+
def _is_file_path(path) -> bool:
219282
try:
220283
pathlib.Path(path)
221284
return True

integration/test_auth.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,13 +898,24 @@ class CredentialWrapper(credentials.Base):
898898

899899
def __init__(self, token):
900900
self._delegate = google.oauth2.credentials.Credentials(token)
901+
self._delegate_async = google.oauth2._credentials_async.Credentials(token)
901902

902903
def get_credential(self):
903904
return self._delegate
904905

906+
def get_credential_async(self):
907+
return self._delegate_async
908+
905909
@classmethod
906910
def from_existing_credential(cls, google_cred):
907911
if not google_cred.token:
908912
request = transport.requests.Request()
909913
google_cred.refresh(request)
910914
return CredentialWrapper(google_cred.token)
915+
916+
@classmethod
917+
async def from_existing_credential_async(cls, google_cred):
918+
if not google_cred.token:
919+
request = transport._aiohttp_requests.Request()
920+
await google_cred.refresh(request)
921+
return CredentialWrapper(google_cred.token)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pytest >= 6.2.0
44
pytest-cov >= 2.4.0
55
pytest-localserver >= 0.4.1
66

7+
aiohttp >= 3.8.1
78
cachecontrol >= 0.12.6
89
google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy'
910
google-api-python-client >= 1.7.8

0 commit comments

Comments
 (0)