Skip to content

Migrating the Python Admin SDK to google-auth Library #19

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

Merged
merged 6 commits into from
May 5, 2017
Merged
Show file tree
Hide file tree
Changes from 5 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
36 changes: 18 additions & 18 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,18 @@
import threading
import time

from oauth2client import crypt
from google.auth import jwt
from google.auth.transport import requests
import google.oauth2.id_token
import six

import firebase_admin
from firebase_admin import credentials
from firebase_admin import jwt

_auth_lock = threading.Lock()

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

_AUTH_ATTRIBUTE = '_auth'
GCLOUD_PROJECT_ENV_VAR = 'GCLOUD_PROJECT'
Expand Down Expand Up @@ -196,7 +195,7 @@ def create_custom_token(self, uid, developer_claims=None):
if developer_claims is not None:
payload['claims'] = developer_claims

return jwt.encode(payload, self._app.credential.signer)
return jwt.encode(self._app.credential.signer, payload)

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

try:
project_id = self._app.credential.project_id
if project_id is None:
project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR)
except AttributeError:
project_id = os.environ.get(GCLOUD_PROJECT_ENV_VAR)

if not project_id:
raise ValueError('Must initialize app with a credentials.Certificate '
'or set your Firebase project ID as the '
'GCLOUD_PROJECT environment variable to call '
'verify_id_token().')
raise ValueError('Failed to ascertain project ID from the credential or the '
'environment. Must initialize app with a credentials.Certificate or '
'set your Firebase project ID as the GCLOUD_PROJECT environment '
'variable to call verify_id_token().')

header, payload = jwt.decode(id_token)
header = jwt.decode_header(id_token)
payload = jwt.decode(id_token, verify=False)
issuer = payload.get('iss')
audience = payload.get('aud')
subject = payload.get('sub')
Expand Down Expand Up @@ -286,13 +288,11 @@ def verify_id_token(self, id_token):
'characters. ') + verify_id_token_msg

if error_message:
raise crypt.AppIdentityError(error_message)
raise ValueError(error_message)

verified_claims = jwt.verify_id_token(
verified_claims = google.oauth2.id_token.verify_firebase_token(
id_token,
self.FIREBASE_CERT_URI,
audience=project_id,
kid=header.get('kid'),
http=_http)
request=_request,
audience=project_id)
verified_claims['uid'] = verified_claims['sub']
return verified_claims
93 changes: 48 additions & 45 deletions firebase_admin/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@
# limitations under the License.

"""Firebase credentials module."""
import collections
import json

import httplib2
import google.auth
from google.auth.transport import requests
from google.oauth2 import credentials
from google.oauth2 import service_account

from oauth2client import client
from oauth2client import crypt

_request = requests.Request()

_http = httplib2.Http()

AccessTokenInfo = collections.namedtuple(
'AccessTokenInfo', ['access_token', 'expiry'])


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

_CREDENTIAL_TYPE = 'service_account'

def __init__(self, file_path):
"""Initializes a credential from a certificate file.

Expand All @@ -53,51 +60,40 @@ def __init__(self, file_path):
ValueError: If the certificate file is invalid.
"""
super(Certificate, self).__init__()
# TODO(hkj): Clean this up once we are able to take a dependency
# TODO(hkj): on latest oauth2client.
with open(file_path) as json_keyfile:
json_data = json.load(json_keyfile)
if json_data.get('type') != client.SERVICE_ACCOUNT:
if json_data.get('type') != self._CREDENTIAL_TYPE:
raise ValueError('Invalid certificate file. File must contain a '
'"type" field set to "{0}".'.format(client.SERVICE_ACCOUNT))
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
self._project_id = json_data.get('project_id')
self._service_account_email = json_data.get('client_email')
try:
self._signer = crypt.Signer.from_string(json_data.get('private_key'))
except Exception as error:
raise ValueError('Failed to parse the private key string or initialize an '
'RSA signer. Caused by: "{0}".'.format(error))
try:
self._g_credential = client.GoogleCredentials.from_stream(file_path)
except client.ApplicationDefaultCredentialsError as error:
raise ValueError('Failed to initialize a certificate credential from file "{0}". '
'Caused by: "{1}"'.format(file_path, error))
self._g_credential = service_account.Credentials.from_service_account_info(json_data)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would appear that any exception thrown from this method will no longer have the filename in the error message, which is useful. Could you wrap this in a try/except similar to the previous version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


@property
def project_id(self):
return self._project_id

@property
def signer(self):
return self._signer
return self._g_credential.signer

@property
def service_account_email(self):
return self._service_account_email
return self._g_credential.service_account_email

def get_access_token(self):
"""Fetches a Google OAuth2 access token using this certificate credential.

Returns:
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
AccessTokenInfo: An access token obtained using the credential.
"""
return self._g_credential.get_access_token(_http)
self._g_credential.refresh(_request)
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)

def get_credential(self):
"""Returns the underlying Google credential.

Returns:
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
google.auth.credentials.Credentials: A Google Auth credential instance."""
return self._g_credential


Expand All @@ -108,31 +104,38 @@ def __init__(self):
"""Initializes the Application Default credentials for the current environment.

Raises:
oauth2client.client.ApplicationDefaultCredentialsError: If Application Default
google.auth.exceptions.DefaultCredentialsError: If Application Default
credentials cannot be initialized in the current environment.
"""
super(ApplicationDefault, self).__init__()
self._g_credential = client.GoogleCredentials.get_application_default()
self._g_credential, self._project_id = google.auth.default()

def get_access_token(self):
"""Fetches a Google OAuth2 access token using this application default credential.

Returns:
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
AccessTokenInfo: An access token obtained using the credential.
"""
return self._g_credential.get_access_token(_http)
self._g_credential.refresh(_request)
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)

def get_credential(self):
"""Returns the underlying Google credential.

Returns:
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
google.auth.credentials.Credentials: A Google Auth credential instance."""
return self._g_credential

@property
def project_id(self):
return self._project_id


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

_CREDENTIAL_TYPE = 'authorized_user'

def __init__(self, file_path):
"""Initializes a refresh token credential from the specified JSON file.

Expand All @@ -146,41 +149,41 @@ def __init__(self, file_path):
super(RefreshToken, self).__init__()
with open(file_path) as json_keyfile:
json_data = json.load(json_keyfile)
if json_data.get('type') != client.AUTHORIZED_USER:
if json_data.get('type') != self._CREDENTIAL_TYPE:
raise ValueError('Invalid refresh token file. File must contain a '
'"type" field set to "{0}".'.format(client.AUTHORIZED_USER))
self._client_id = json_data.get('client_id')
self._client_secret = json_data.get('client_secret')
self._refresh_token = json_data.get('refresh_token')
try:
self._g_credential = client.GoogleCredentials.from_stream(file_path)
except client.ApplicationDefaultCredentialsError as error:
raise ValueError('Failed to initialize a refresh token credential from file "{0}". '
'Caused by: "{1}".'.format(file_path, error))
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
client_id = json_data.get('client_id')
client_secret = json_data.get('client_secret')
refresh_token = json_data.get('refresh_token')
self._g_credential = credentials.Credentials(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This similarly loses file information for error messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Credentials constructor does not raise any errors. So I've implemented some error handling in our init method to check for the required JSON keys, and raise a ValueError with the file name in the error message.

token=None, refresh_token=refresh_token,
token_uri='https://accounts.google.com/o/oauth2/token',
client_id=client_id, client_secret=client_secret)

@property
def client_id(self):
return self._client_id
return self._g_credential.client_id

@property
def client_secret(self):
return self._client_secret
return self._g_credential.client_secret

@property
def refresh_token(self):
return self._refresh_token
return self._g_credential.refresh_token

def get_access_token(self):
"""Fetches a Google OAuth2 access token using this refresh token credential.

Returns:
oauth2client.client.AccessTokenInfo: An access token obtained via oauth2client.
AccessTokenInfo: An access token obtained using the credential.
"""
return self._g_credential.get_access_token(_http)
self._g_credential.refresh(_request)
return AccessTokenInfo(self._g_credential.token, self._g_credential.expiry)

def get_credential(self):
"""Returns the underlying Google credential.

Returns:
oauth2client.client.GoogleCredentials: An oauth2client credential instance."""
google.auth.credentials.Credentials: A Google Auth credential instance."""
return self._g_credential
Loading