Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(credentials): Add async credentials. #629

Merged
merged 7 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 99 additions & 37 deletions firebase_admin/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@
import json
import pathlib

import google.auth
from google.auth.transport import requests
from google.oauth2 import credentials
from typing import (
Type,
List
)

import google.auth # type: ignore
from google.auth import default
from google.auth._default_async import default_async # type: ignore
from google.auth.transport import requests # type: ignore
from google.auth.transport import _aiohttp_requests as aiohttp_requests
from google.oauth2 import credentials # type: ignore
from google.oauth2 import _credentials_async as credentials_async
from google.oauth2 import service_account
from google.oauth2 import _service_account_async as service_account_async


_request = requests.Request()
_scopes = [
_request: requests.Request = requests.Request()
_request_async: aiohttp_requests.Request = aiohttp_requests.Request()
_scopes: List[str] = [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/devstorage.read_write',
Expand All @@ -33,7 +44,7 @@
'https://www.googleapis.com/auth/userinfo.email'
]

AccessTokenInfo = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
AccessTokenInfo: Type[tuple] = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
"""Data included in an OAuth2 access token.

Contains the access token string and the expiry time. The expirty time is exposed as a
Expand All @@ -44,8 +55,8 @@
class Base:
"""Provides OAuth2 access tokens for accessing Firebase services."""

def get_access_token(self):
"""Fetches a Google OAuth2 access token using this credential instance.
def get_access_token(self) -> tuple:
"""Fetches a Google OAuth2 access token using the synchronous credential instance.

Returns:
AccessTokenInfo: An access token obtained using the credential.
Expand All @@ -54,8 +65,22 @@ def get_access_token(self):
google_cred.refresh(_request)
return AccessTokenInfo(google_cred.token, google_cred.expiry)

async def get_access_token_async(self) -> tuple:
"""Fetches a Google OAuth2 access token using the asynchronous credential instance.

Returns:
AccessTokenInfo: An access token obtained using the credential.
"""
google_cred = self.get_credential_async()
await google_cred.refresh(_request_async)
return AccessTokenInfo(google_cred.token, google_cred.expiry)

def get_credential(self):
"""Returns the Google credential instance used for authentication."""
"""Returns the Google synchronous credential instance used for authentication."""
raise NotImplementedError

def get_credential_async(self):
"""Returns the Google asynchronous credential instance used for authentication."""
raise NotImplementedError


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

_CREDENTIAL_TYPE = 'service_account'

def __init__(self, cert):
"""Initializes a credential from a Google service account certificate.
def __init__(self, cert: str) -> None:
"""Initializes credentials from a Google service account certificate.

Service account certificates can be downloaded as JSON files from the Firebase console.
To instantiate a credential from a certificate file, either specify the file path or a
Expand Down Expand Up @@ -95,44 +120,54 @@ def __init__(self, cert):
try:
self._g_credential = service_account.Credentials.from_service_account_info(
json_data, scopes=_scopes)
self._g_credential_async = service_account_async.Credentials.from_service_account_info(
json_data, scopes=_scopes)
except ValueError as error:
raise ValueError('Failed to initialize a certificate credential. '
'Caused by: "{0}"'.format(error))

@property
def project_id(self):
def project_id(self) -> str:
return self._g_credential.project_id

@property
def signer(self):
def signer(self) -> google.auth.crypt.Signer:
return self._g_credential.signer

@property
def service_account_email(self):
def service_account_email(self) -> str:
return self._g_credential.service_account_email

def get_credential(self):
"""Returns the underlying Google credential.
def get_credential(self) -> service_account.Credentials:
"""Returns the underlying Google synchronous credential.

Returns:
google.auth.credentials.Credentials: A Google Auth credential instance."""
google.auth.credentials.Credentials: A Google Auth synchronous credential instance."""
return self._g_credential

def get_credential_async(self) -> service_account_async.Credentials:
"""Returns the underlying Google asynchronous credential.

Returns:
google.auth._credentials_async.Credentials: A Google Auth asynchronous credential
instance."""
return self._g_credential_async

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

def __init__(self):
def __init__(self) -> None:
"""Creates an instance that will use Application Default credentials.

The credentials will be lazily initialized when get_credential() or
project_id() is called. See those methods for possible errors raised.
The credentials will be lazily initialized when get_credential(), get_credential_async()
jkyle109 marked this conversation as resolved.
Show resolved Hide resolved
or project_id() is called. See those methods for possible errors raised.
"""
super(ApplicationDefault, self).__init__()
self._g_credential = None # Will be lazily-loaded via _load_credential().
self._g_credential_async = None # Will be lazily-loaded via _load_credential_async().

def get_credential(self):
"""Returns the underlying Google credential.
def get_credential(self) -> credentials.Credentials:
"""Returns the underlying Google synchronous credential.

Raises:
google.auth.exceptions.DefaultCredentialsError: If Application Default
Expand All @@ -142,9 +177,20 @@ def get_credential(self):
self._load_credential()
return self._g_credential

def get_credential_async(self) -> credentials_async.Credentials:
"""Returns the underlying Google asynchronous credential.

Raises:
google.auth.exceptions.DefaultCredentialsError: If Application Default
credentials cannot be initialized in the current environment.
Returns:
google.auth._credentials_async.Credentials: A Google Auth credential instance."""
self._load_credential_async()
return self._g_credential_async

@property
def project_id(self):
"""Returns the project_id from the underlying Google credential.
def project_id(self) -> str:
"""Returns the project_id from the underlying Google credentials.

Raises:
google.auth.exceptions.DefaultCredentialsError: If Application Default
Expand All @@ -154,21 +200,25 @@ def project_id(self):
self._load_credential()
return self._project_id

def _load_credential(self):
def _load_credential(self) -> None:
if not self._g_credential:
self._g_credential, self._project_id = google.auth.default(scopes=_scopes)
self._g_credential, self._project_id = default(scopes=_scopes)

def _load_credential_async(self) -> None:
if not self._g_credential_async:
self._g_credential_async, self._project_id = default_async(scopes=_scopes)

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

_CREDENTIAL_TYPE = 'authorized_user'

def __init__(self, refresh_token):
"""Initializes a credential from a refresh token JSON file.
def __init__(self, refresh_token: str) -> None:
"""Initializes credentials from a refresh token JSON file.

The JSON must consist of client_id, client_secret and refresh_token fields. Refresh
token files are typically created and managed by the gcloud SDK. To instantiate
a credential from a refresh token file, either specify the file path or a dict
credentials from a refresh token file, either specify the file path or a dict
representing the parsed contents of the file.

Args:
Expand All @@ -194,28 +244,40 @@ def __init__(self, refresh_token):
raise ValueError('Invalid refresh token configuration. JSON must contain a '
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
self._g_credential = credentials.Credentials.from_authorized_user_info(json_data, _scopes)
self._g_credential_async = credentials_async.Credentials.from_authorized_user_info(
json_data,
_scopes
)

@property
def client_id(self):
def client_id(self) -> str:
return self._g_credential.client_id

@property
def client_secret(self):
def client_secret(self) -> str:
return self._g_credential.client_secret

@property
def refresh_token(self):
def refresh_token(self) -> str:
return self._g_credential.refresh_token

def get_credential(self):
"""Returns the underlying Google credential.
def get_credential(self) -> credentials.Credentials:
"""Returns the underlying Google synchronous credential.

Returns:
google.auth.credentials.Credentials: A Google Auth credential instance."""
google.auth.credentials.Credentials: A Google Auth synchronous credential instance."""
return self._g_credential

def get_credential_async(self) -> credentials_async.Credentials:
"""Returns the underlying Google asynchronous credential.

Returns:
google.auth._credentials_async.Credentials: A Google Auth asynchronous credential
instance."""
return self._g_credential_async


def _is_file_path(path):
def _is_file_path(path) -> bool:
try:
pathlib.Path(path)
return True
Expand Down
11 changes: 11 additions & 0 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,13 +898,24 @@ class CredentialWrapper(credentials.Base):

def __init__(self, token):
self._delegate = google.oauth2.credentials.Credentials(token)
self._delegate_async = google.oauth2._credentials_async.Credentials(token)

def get_credential(self):
return self._delegate

def get_credential_async(self):
return self._delegate_async

@classmethod
def from_existing_credential(cls, google_cred):
if not google_cred.token:
request = transport.requests.Request()
google_cred.refresh(request)
return CredentialWrapper(google_cred.token)

@classmethod
async def from_existing_credential_async(cls, google_cred):
if not google_cred.token:
request = transport._aiohttp_requests.Request()
await google_cred.refresh(request)
return CredentialWrapper(google_cred.token)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pytest >= 6.2.0
pytest-cov >= 2.4.0
pytest-localserver >= 0.4.1

aiohttp == 3.8.1
jkyle109 marked this conversation as resolved.
Show resolved Hide resolved
cachecontrol >= 0.12.6
google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy'
google-api-python-client >= 1.7.8
Expand Down