Skip to content

Commit 9b679d0

Browse files
authored
Implementing App Default and Refresh Token Credential Types (#6)
* Implementing app default and refresh token credential types * Added properties to refresh token class * Adding error handling; Removing the old exception wrapping syntax since it does not work on python 3. * Updated docstring * Updated comments and docstrings
1 parent e937f28 commit 9b679d0

File tree

3 files changed

+161
-12
lines changed

3 files changed

+161
-12
lines changed

firebase_admin/credentials.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Firebase credentials module."""
22
import json
3-
import sys
43

54
import httplib2
65

@@ -41,24 +40,28 @@ def __init__(self, file_path):
4140
4241
Raises:
4342
IOError: If the specified file doesn't exist or cannot be read.
44-
ValueError: If an error occurs while parsing the file content.
43+
ValueError: If the certificate file is invalid.
4544
"""
4645
super(Certificate, self).__init__()
4746
# TODO(hkj): Clean this up once we are able to take a dependency
4847
# TODO(hkj): on latest oauth2client.
4948
with open(file_path) as json_keyfile:
5049
json_data = json.load(json_keyfile)
50+
if json_data.get('type') != client.SERVICE_ACCOUNT:
51+
raise ValueError('Invalid certificate file. File must contain a '
52+
'"type" field set to "{0}".'.format(client.SERVICE_ACCOUNT))
5153
self._project_id = json_data.get('project_id')
54+
self._service_account_email = json_data.get('client_email')
5255
try:
53-
self._signer = crypt.Signer.from_string(
54-
json_data.get('private_key'))
56+
self._signer = crypt.Signer.from_string(json_data.get('private_key'))
5557
except Exception as error:
56-
err_type, err_value, err_traceback = sys.exc_info()
57-
err_message = 'Failed to parse the private key string: {0}'.format(
58-
error)
59-
raise ValueError, (err_message, err_type, err_value), err_traceback
60-
self._service_account_email = json_data.get('client_email')
61-
self._g_credential = client.GoogleCredentials.from_stream(file_path)
58+
raise ValueError('Failed to parse the private key string or initialize an '
59+
'RSA signer. Caused by: "{0}".'.format(error))
60+
try:
61+
self._g_credential = client.GoogleCredentials.from_stream(file_path)
62+
except client.ApplicationDefaultCredentialsError as error:
63+
raise ValueError('Failed to initialize a certificate credential from file "{0}". '
64+
'Caused by: "{1}"'.format(file_path, error))
6265

6366
@property
6467
def project_id(self):
@@ -77,3 +80,70 @@ def get_access_token(self):
7780

7881
def get_credential(self):
7982
return self._g_credential
83+
84+
85+
class ApplicationDefault(Base):
86+
"""A Google Application Default credential."""
87+
88+
def __init__(self):
89+
"""Initializes the Application Default credentials for the current environment.
90+
91+
Raises:
92+
oauth2client.client.ApplicationDefaultCredentialsError: If Application Default
93+
credentials cannot be initialized in the current environment.
94+
"""
95+
super(ApplicationDefault, self).__init__()
96+
self._g_credential = client.GoogleCredentials.get_application_default()
97+
98+
def get_access_token(self):
99+
return self._g_credential.get_access_token(_http)
100+
101+
def get_credential(self):
102+
return self._g_credential
103+
104+
105+
class RefreshToken(Base):
106+
"""A credential initialized from an existing refresh token."""
107+
108+
def __init__(self, file_path):
109+
"""Initializes a refresh token credential from the specified JSON file.
110+
111+
Args:
112+
file_path: File path to a refresh token JSON file.
113+
114+
Raises:
115+
IOError: If the specified file doesn't exist or cannot be read.
116+
ValueError: If the refresh token file is invalid.
117+
"""
118+
super(RefreshToken, self).__init__()
119+
with open(file_path) as json_keyfile:
120+
json_data = json.load(json_keyfile)
121+
if json_data.get('type') != client.AUTHORIZED_USER:
122+
raise ValueError('Invalid refresh token file. File must contain a '
123+
'"type" field set to "{0}".'.format(client.AUTHORIZED_USER))
124+
self._client_id = json_data.get('client_id')
125+
self._client_secret = json_data.get('client_secret')
126+
self._refresh_token = json_data.get('refresh_token')
127+
try:
128+
self._g_credential = client.GoogleCredentials.from_stream(file_path)
129+
except client.ApplicationDefaultCredentialsError as error:
130+
raise ValueError('Failed to initialize a refresh token credential from file "{0}". '
131+
'Caused by: "{1}".'.format(file_path, error))
132+
133+
@property
134+
def client_id(self):
135+
return self._client_id
136+
137+
@property
138+
def client_secret(self):
139+
return self._client_secret
140+
141+
@property
142+
def refresh_token(self):
143+
return self._refresh_token
144+
145+
def get_access_token(self):
146+
return self._g_credential.get_access_token(_http)
147+
148+
def get_credential(self):
149+
return self._g_credential

tests/data/refresh_token.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "authorized_user",
3+
"client_id": "mock.apps.googleusercontent.com",
4+
"client_secret": "mock-secret",
5+
"refresh_token": "mock-refresh-token"
6+
}

tests/test_credentials.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
"""Tests for firebase_admin.credentials module."""
2+
import json
3+
import os
4+
25
from firebase_admin import credentials
36
from oauth2client import client
47
from oauth2client import crypt
@@ -20,14 +23,84 @@ def test_init_from_file(self):
2023
assert isinstance(g_credential, client.GoogleCredentials)
2124
assert g_credential.access_token is None
2225

26+
# The HTTP client should not be used or referenced.
27+
credentials._http = 'unused'
28+
access_token = credential.get_access_token()
29+
assert isinstance(access_token.access_token, basestring)
30+
assert isinstance(access_token.expires_in, int)
31+
32+
def test_init_from_nonexisting_file(self):
33+
with pytest.raises(IOError):
34+
credentials.Certificate(
35+
testutils.resource_filename('non_existing.json'))
36+
37+
def test_init_from_invalid_file(self):
38+
with pytest.raises(ValueError):
39+
credentials.Certificate(
40+
testutils.resource_filename('refresh_token.json'))
41+
42+
43+
@pytest.fixture
44+
def app_default(request):
45+
file_path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')
46+
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = request.param
47+
yield
48+
if file_path:
49+
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = file_path
50+
51+
52+
class TestApplicationDefault(object):
53+
54+
@pytest.mark.parametrize('app_default', [testutils.resource_filename('service_account.json')],
55+
indirect=True)
56+
def test_init(self, app_default): # pylint: disable=unused-argument
57+
credential = credentials.ApplicationDefault()
58+
g_credential = credential.get_credential()
59+
assert isinstance(g_credential, client.GoogleCredentials)
60+
assert g_credential.access_token is None
61+
2362
# The HTTP client should not be used.
24-
credential._http = None
63+
credentials._http = 'unused'
2564
access_token = credential.get_access_token()
2665
assert isinstance(access_token.access_token, basestring)
2766
assert isinstance(access_token.expires_in, int)
2867

68+
@pytest.mark.parametrize('app_default', [testutils.resource_filename('non_existing.json')],
69+
indirect=True)
70+
def test_nonexisting_path(self, app_default): # pylint: disable=unused-argument
71+
with pytest.raises(client.ApplicationDefaultCredentialsError):
72+
credentials.ApplicationDefault()
73+
74+
75+
class TestRefreshToken(object):
76+
77+
def test_init_from_file(self):
78+
credential = credentials.RefreshToken(
79+
testutils.resource_filename('refresh_token.json'))
80+
assert credential.client_id == 'mock.apps.googleusercontent.com'
81+
assert credential.client_secret == 'mock-secret'
82+
assert credential.refresh_token == 'mock-refresh-token'
83+
84+
g_credential = credential.get_credential()
85+
assert isinstance(g_credential, client.GoogleCredentials)
86+
assert g_credential.access_token is None
87+
88+
mock_response = {
89+
'access_token': 'mock_access_token',
90+
'expires_in': 1234
91+
}
92+
credentials._http = testutils.HttpMock(200, json.dumps(mock_response))
93+
access_token = credential.get_access_token()
94+
assert access_token.access_token == 'mock_access_token'
95+
# GoogleCredentials class recalculates the expires_in property before returning.
96+
assert access_token.expires_in <= 1234
2997

3098
def test_init_from_nonexisting_file(self):
3199
with pytest.raises(IOError):
32-
credentials.Certificate(
100+
credentials.RefreshToken(
33101
testutils.resource_filename('non_existing.json'))
102+
103+
def test_init_from_invalid_file(self):
104+
with pytest.raises(ValueError):
105+
credentials.RefreshToken(
106+
testutils.resource_filename('service_account.json'))

0 commit comments

Comments
 (0)