Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Support token_uri and revoke_uri in ServiceAccountCredentials #510

Merged
merged 6 commits into from
Jun 7, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
98 changes: 78 additions & 20 deletions oauth2client/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class ServiceAccountCredentials(AssertionCredentials):
service account.
user_agent: string, (Optional) User agent to use when sending
request.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

This comment was marked as spam.

This comment was marked as spam.

kwargs: dict, Extra key-value pairs (both strings) to send in the
payload body when making an assertion.
"""
Expand All @@ -106,10 +110,13 @@ def __init__(self,
private_key_id=None,
client_id=None,
user_agent=None,
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
**kwargs):

super(ServiceAccountCredentials, self).__init__(
None, user_agent=user_agent)
None, user_agent=user_agent, token_uri=token_uri,
revoke_uri=revoke_uri)

self._service_account_email = service_account_email
self._signer = signer
Expand Down Expand Up @@ -145,14 +152,20 @@ def _to_json(self, strip, to_serialize=None):
strip, to_serialize=to_serialize)

@classmethod
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes,
token_uri=GOOGLE_TOKEN_URI,

This comment was marked as spam.

This comment was marked as spam.

revoke_uri=GOOGLE_REVOKE_URI):
"""Helper for factory constructors from JSON keyfile.

Args:
keyfile_dict: dict-like object, The parsed dictionary-like object
containing the contents of the JSON keyfile.
scopes: List or string, Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -176,18 +189,26 @@ def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
credentials = cls(service_account_email, signer, scopes=scopes,
private_key_id=private_key_id,
client_id=client_id)
client_id=client_id, token_uri=token_uri,
revoke_uri=revoke_uri)
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
return credentials

@classmethod
def from_json_keyfile_name(cls, filename, scopes=''):
def from_json_keyfile_name(cls, filename, scopes='',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):

"""Factory constructor from JSON keyfile by name.

Args:
filename: string, The location of the keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

This comment was marked as spam.


Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -200,17 +221,25 @@ def from_json_keyfile_name(cls, filename, scopes=''):
"""
with open(filename, 'r') as file_obj:
client_credentials = json.load(file_obj)
return cls._from_parsed_json_keyfile(client_credentials, scopes)
return cls._from_parsed_json_keyfile(client_credentials, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)

@classmethod
def from_json_keyfile_dict(cls, keyfile_dict, scopes=''):
def from_json_keyfile_dict(cls, keyfile_dict, scopes='',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):
"""Factory constructor from parsed JSON keyfile.

Args:
keyfile_dict: dict-like object, The parsed dictionary-like object
containing the contents of the JSON keyfile.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

This comment was marked as spam.


Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -221,12 +250,16 @@ def from_json_keyfile_dict(cls, keyfile_dict, scopes=''):
KeyError, if one of the expected keys is not present in
the keyfile.
"""
return cls._from_parsed_json_keyfile(keyfile_dict, scopes)
return cls._from_parsed_json_keyfile(keyfile_dict, scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)

@classmethod
def _from_p12_keyfile_contents(cls, service_account_email,
private_key_pkcs12,
private_key_password=None, scopes=''):
def from_p12_keyfile_contents(cls, service_account_email,
private_key_pkcs12,
private_key_password=None, scopes='',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile.

Args:
Expand All @@ -237,6 +270,10 @@ def _from_p12_keyfile_contents(cls, service_account_email,
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -252,14 +289,18 @@ def _from_p12_keyfile_contents(cls, service_account_email,
raise NotImplementedError(_PKCS12_ERROR)
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)
credentials = cls(service_account_email, signer, scopes=scopes)
credentials = cls(service_account_email, signer, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)
credentials._private_key_pkcs12 = private_key_pkcs12
credentials._private_key_password = private_key_password
return credentials

@classmethod
def from_p12_keyfile(cls, service_account_email, filename,
private_key_password=None, scopes=''):
private_key_password=None, scopes='',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):

"""Factory constructor from JSON keyfile.

Args:
Expand All @@ -270,6 +311,10 @@ def from_p12_keyfile(cls, service_account_email, filename,
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -281,13 +326,16 @@ def from_p12_keyfile(cls, service_account_email, filename,
"""
with open(filename, 'rb') as file_obj:
private_key_pkcs12 = file_obj.read()
return cls._from_p12_keyfile_contents(
return cls.from_p12_keyfile_contents(
service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes)
private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)

@classmethod
def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
private_key_password=None, scopes=''):
private_key_password=None, scopes='',
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):
"""Factory constructor from JSON keyfile.

Args:
Expand All @@ -299,6 +347,10 @@ def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
token_uri: string, URI for token endpoint. For convenience defaults
to Google's endpoints but any OAuth 2.0 provider can be
used.
revoke_uri: string, URI for revoke endpoint.

Returns:
ServiceAccountCredentials, a credentials object created from
Expand All @@ -309,9 +361,10 @@ def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
active crypto library.
"""
private_key_pkcs12 = file_buffer.read()
return cls._from_p12_keyfile_contents(
return cls.from_p12_keyfile_contents(
service_account_email, private_key_pkcs12,
private_key_password=private_key_password, scopes=scopes)
private_key_password=private_key_password, scopes=scopes,
token_uri=token_uri, revoke_uri=revoke_uri)

def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
Expand Down Expand Up @@ -508,6 +561,8 @@ def __init__(self,
private_key_id=None,
client_id=None,
user_agent=None,
token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI,
additional_claims=None):
if additional_claims is None:
additional_claims = {}
Expand All @@ -517,6 +572,8 @@ def __init__(self,
private_key_id=private_key_id,
client_id=client_id,
user_agent=user_agent,
token_uri=token_uri,
revoke_uri=revoke_uri,
**additional_claims)

def authorize(self, http):
Expand Down Expand Up @@ -595,17 +652,18 @@ def create_scoped_required(self):
# JWTAccessCredentials are unscoped by definition
return True

def create_scoped(self, scopes):
def create_scoped(self, scopes, token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI):
# Returns an OAuth2 credentials with the given scope
result = ServiceAccountCredentials(self._service_account_email,
self._signer,
scopes=scopes,
private_key_id=self._private_key_id,
client_id=self.client_id,
user_agent=self._user_agent,
token_uri=token_uri,
revoke_uri=revoke_uri,
**self._kwargs)
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
if self._private_key_pkcs8_pem is not None:
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
if self._private_key_pkcs12 is not None:
Expand Down
56 changes: 39 additions & 17 deletions tests/test_service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,16 @@ def test_service_account_email(self):
self.credentials.service_account_email)

@staticmethod
def _from_json_keyfile_name_helper(payload, scopes=None):
def _from_json_keyfile_name_helper(payload, scopes=None,
token_uri=None, revoke_uri=None):
filehandle, filename = tempfile.mkstemp()
os.close(filehandle)
try:
with open(filename, 'w') as file_obj:
json.dump(payload, file_obj)
return ServiceAccountCredentials.from_json_keyfile_name(
filename, scopes=scopes)
filename, scopes=scopes, token_uri=token_uri,
revoke_uri=revoke_uri)
finally:
os.remove(filename)

Expand All @@ -122,13 +124,19 @@ def test_from_json_keyfile_name_factory(self, signer_factory):
'private_key': private_key,
}
scopes = ['foo', 'bar']
creds = self._from_json_keyfile_name_helper(payload, scopes=scopes)
token_uri = 'baz'
revoke_uri = 'qux'
creds = self._from_json_keyfile_name_helper(payload, scopes=scopes,
token_uri=token_uri,
revoke_uri=revoke_uri)
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertEqual(creds.client_id, client_id)
self.assertEqual(creds._service_account_email, client_email)
self.assertEqual(creds._private_key_id, private_key_id)
self.assertEqual(creds._private_key_pkcs8_pem, private_key)
self.assertEqual(creds._scopes, ' '.join(scopes))
self.assertEqual(creds.token_uri, token_uri)
self.assertEqual(creds.revoke_uri, revoke_uri)
# Check stub.
self.assertEqual(creds._signer, signer_factory.return_value)
signer_factory.assert_called_once_with(private_key)
Expand All @@ -148,24 +156,33 @@ def test_from_json_keyfile_name_factory_missing_field(self):
with self.assertRaises(KeyError):
self._from_json_keyfile_name_helper(payload)

def _from_p12_keyfile_helper(self, private_key_password=None, scopes=''):
def _from_p12_keyfile_helper(self, private_key_password=None, scopes='',
token_uri=None, revoke_uri=None):
service_account_email = '[email protected]'
filename = data_filename('privatekey.p12')
with open(filename, 'rb') as file_obj:
key_contents = file_obj.read()
creds = ServiceAccountCredentials.from_p12_keyfile(
creds_from_filename = ServiceAccountCredentials.from_p12_keyfile(
service_account_email, filename,
private_key_password=private_key_password,
scopes=scopes)
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertIsNone(creds.client_id)
self.assertEqual(creds._service_account_email, service_account_email)
self.assertIsNone(creds._private_key_id)
self.assertIsNone(creds._private_key_pkcs8_pem)
self.assertEqual(creds._private_key_pkcs12, key_contents)
if private_key_password is not None:
self.assertEqual(creds._private_key_password, private_key_password)
self.assertEqual(creds._scopes, ' '.join(scopes))
scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri)
creds_from_file_contents = (
ServiceAccountCredentials.from_p12_keyfile_contents(
service_account_email, key_contents,
private_key_password=private_key_password,
scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri))
for creds in (creds_from_filename, creds_from_file_contents):
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertIsNone(creds.client_id)
self.assertEqual(creds._service_account_email, service_account_email)
self.assertIsNone(creds._private_key_id)
self.assertIsNone(creds._private_key_pkcs8_pem)
self.assertEqual(creds._private_key_pkcs12, key_contents)
if private_key_password is not None:
self.assertEqual(creds._private_key_password, private_key_password)
self.assertEqual(creds._scopes, ' '.join(scopes))
self.assertEqual(creds.token_uri, token_uri)
self.assertEqual(creds.revoke_uri, revoke_uri)

def _p12_not_implemented_helper(self):
service_account_email = '[email protected]'
Expand All @@ -188,13 +205,16 @@ def test_from_p12_keyfile_defaults(self):
def test_from_p12_keyfile_explicit(self):
password = 'notasecret'
self._from_p12_keyfile_helper(private_key_password=password,
scopes=['foo', 'bar'])
scopes=['foo', 'bar'],
token_uri='baz', revoke_uri='qux')

def test_from_p12_keyfile_buffer(self):
service_account_email = '[email protected]'
filename = data_filename('privatekey.p12')
private_key_password = 'notasecret'
scopes = ['foo', 'bar']
token_uri = 'baz'
revoke_uri = 'qux'
with open(filename, 'rb') as file_obj:
key_contents = file_obj.read()
# Seek back to the beginning so the buffer can be
Expand All @@ -203,7 +223,7 @@ def test_from_p12_keyfile_buffer(self):
creds = ServiceAccountCredentials.from_p12_keyfile_buffer(
service_account_email, file_obj,
private_key_password=private_key_password,
scopes=scopes)
scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri)
# Check the created object.
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertIsNone(creds.client_id)
Expand All @@ -213,6 +233,8 @@ def test_from_p12_keyfile_buffer(self):
self.assertEqual(creds._private_key_pkcs12, key_contents)
self.assertEqual(creds._private_key_password, private_key_password)
self.assertEqual(creds._scopes, ' '.join(scopes))
self.assertEqual(creds.token_uri, token_uri)
self.assertEqual(creds.revoke_uri, revoke_uri)

def test_create_scoped_required_without_scopes(self):
self.assertTrue(self.credentials.create_scoped_required())
Expand Down