Skip to content
This repository has been archived by the owner on Jan 18, 2025. It is now read-only.

Commit

Permalink
Merge pull request #398 from dhermes/consolidate-service-accounts-v3
Browse files Browse the repository at this point in the history
Implemented p12 support in ServiceAccountCredentials.
  • Loading branch information
nathanielmanistaatgoogle committed Feb 5, 2016
2 parents 8518131 + d3391bc commit 4418564
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 26 deletions.
33 changes: 19 additions & 14 deletions oauth2client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,32 +256,37 @@ def apply(self, headers):
"""
_abstract()

def _to_json(self, strip):
def _to_json(self, strip, to_serialize=None):
"""Utility function that creates JSON repr. of a Credentials object.
Args:
strip: array, An array of names of members to not include in the
strip: array, An array of names of members to exclude from the
JSON.
to_serialize: dict, (Optional) The properties for this object
that will be serialized. This allows callers to modify
before serializing.
Returns:
string, a JSON representation of this instance, suitable to pass to
from_json().
"""
t = type(self)
d = copy.copy(self.__dict__)
curr_type = self.__class__
if to_serialize is None:
to_serialize = copy.copy(self.__dict__)
for member in strip:
if member in d:
del d[member]
d['token_expiry'] = _parse_expiry(d.get('token_expiry'))
# Add in information we will need later to reconsistitue this instance.
d['_class'] = t.__name__
d['_module'] = t.__module__
for key, val in d.items():
if member in to_serialize:
del to_serialize[member]
to_serialize['token_expiry'] = _parse_expiry(
to_serialize.get('token_expiry'))
# Add in information we will need later to reconstitute this instance.
to_serialize['_class'] = curr_type.__name__
to_serialize['_module'] = curr_type.__module__
for key, val in to_serialize.items():
if isinstance(val, bytes):
d[key] = val.decode('utf-8')
to_serialize[key] = val.decode('utf-8')
if isinstance(val, set):
d[key] = list(val)
return json.dumps(d)
to_serialize[key] = list(val)
return json.dumps(to_serialize)

def to_json(self):
"""Creating a JSON representation of an instance of Credentials.
Expand Down
102 changes: 99 additions & 3 deletions oauth2client/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""oauth2client Service account credentials class."""

import base64
import copy
import datetime
import json
import time
Expand All @@ -31,13 +32,26 @@
from oauth2client import crypt


_PASSWORD_DEFAULT = 'notasecret'
_PKCS12_KEY = '_private_key_pkcs12'
_PKCS12_ERROR = r"""
This library only implements PKCS#12 support via the pyOpenSSL library.
Either install pyOpenSSL, or please convert the .p12 file
to .pem format:
$ cat key.p12 | \
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
> openssl rsa > key.pem
"""


class ServiceAccountCredentials(AssertionCredentials):
"""Service Account credential for OAuth 2.0 signed JWT grants.
Supports
* JSON keyfile (typically contains a PKCS8 key stored as
PEM text)
* ``.p12`` key (stores PKCS12 key and certificate)
Makes an assertion to server using a signed JWT assertion in exchange
for an access token.
Expand Down Expand Up @@ -74,6 +88,8 @@ class ServiceAccountCredentials(AssertionCredentials):
# Can be over-ridden by factory constructors. Used for
# serialization/deserialization purposes.
_private_key_pkcs8_pem = None
_private_key_pkcs12 = None
_private_key_password = None

def __init__(self,
service_account_email,
Expand All @@ -95,6 +111,31 @@ def __init__(self,
self._user_agent = user_agent
self._kwargs = kwargs

def _to_json(self, strip, to_serialize=None):
"""Utility function that creates JSON repr. of a credentials object.
Over-ride is needed since PKCS#12 keys will not in general be JSON
serializable.
Args:
strip: array, An array of names of members to exclude from the
JSON.
to_serialize: dict, (Optional) The properties for this object
that will be serialized. This allows callers to modify
before serializing.
Returns:
string, a JSON representation of this instance, suitable to pass to
from_json().
"""
if to_serialize is None:
to_serialize = copy.copy(self.__dict__)
pkcs12_val = to_serialize.get(_PKCS12_KEY)
if pkcs12_val is not None:
to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
return super(ServiceAccountCredentials, self)._to_json(
strip, to_serialize=to_serialize)

@classmethod
def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
"""Helper for factory constructors from JSON keyfile.
Expand Down Expand Up @@ -174,6 +215,39 @@ def from_json_keyfile_dict(cls, keyfile_dict, scopes=''):
"""
return cls._from_parsed_json_keyfile(keyfile_dict, scopes)

@classmethod
def from_p12_keyfile(cls, service_account_email, filename,
private_key_password=None, scopes=''):
"""Factory constructor from JSON keyfile.
Args:
service_account_email: string, The email associated with the
service account.
filename: string, The location of the PKCS#12 keyfile.
private_key_password: string, (Optional) Password for PKCS#12
private key. Defaults to ``notasecret``.
scopes: List or string, (Optional) Scopes to use when acquiring an
access token.
Returns:
ServiceAccountCredentials, a credentials object created from
the keyfile.
Raises:
NotImplementedError if pyOpenSSL is not installed / not the
active crypto library.
"""
with open(filename, 'rb') as file_obj:
private_key_pkcs12 = file_obj.read()
if private_key_password is None:
private_key_password = _PASSWORD_DEFAULT
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)
credentials = cls(service_account_email, signer, scopes=scopes)
credentials._private_key_pkcs12 = private_key_pkcs12
credentials._private_key_password = private_key_password
return credentials

def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""
now = int(time.time())
Expand All @@ -197,6 +271,7 @@ def service_account_email(self):

@property
def serialization_data(self):
# NOTE: This is only useful for JSON keyfile.
return {
'type': 'service_account',
'client_email': self._service_account_email,
Expand All @@ -221,8 +296,21 @@ def from_json(cls, json_data):
if not isinstance(json_data, dict):
json_data = json.loads(_from_bytes(json_data))

private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
private_key_pkcs8_pem = None
pkcs12_val = json_data.get(_PKCS12_KEY)
password = None
if pkcs12_val is None:
private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
signer = crypt.Signer.from_string(private_key_pkcs8_pem)
else:
# NOTE: This assumes that private_key_pkcs8_pem is not also
# in the serialized data. This would be very incorrect
# state.
pkcs12_val = base64.b64decode(pkcs12_val)
password = json_data['_private_key_password']
signer = crypt.Signer.from_string(private_key_pkcs12,
private_key_password)

credentials = cls(
json_data['_service_account_email'],
signer,
Expand All @@ -232,7 +320,12 @@ def from_json(cls, json_data):
user_agent=json_data['_user_agent'],
**json_data['_kwargs']
)
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
if private_key_pkcs8_pem is not None:
credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
if pkcs12_val is not None:
credentials._private_key_pkcs12 = pkcs12_val
if password is not None:
credentials._private_key_password = password
credentials.invalid = json_data['invalid']
credentials.access_token = json_data['access_token']
credentials.token_uri = json_data['token_uri']
Expand All @@ -256,4 +349,7 @@ def create_scoped(self, scopes):
**self._kwargs)
result.token_uri = self.token_uri
result.revoke_uri = self.revoke_uri
result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
result._private_key_pkcs12 = self._private_key_pkcs12
result._private_key_password = self._private_key_password
return result
11 changes: 2 additions & 9 deletions scripts/run_system_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,8 @@ def run_json():


def run_p12():
with open(P12_KEY_PATH, 'rb') as file_object:
private_key_contents = file_object.read()

credentials = client.SignedJwtAssertionCredentials(
service_account_name=P12_KEY_EMAIL,
private_key=private_key_contents,
scope=SCOPE,
)

credentials = ServiceAccountCredentials.from_p12_keyfile(
P12_KEY_EMAIL, P12_KEY_PATH, scopes=SCOPE)
_check_user_info(credentials, P12_KEY_EMAIL)


Expand Down
27 changes: 27 additions & 0 deletions tests/test_service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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=''):
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(
service_account_email, filename,
private_key_password=private_key_password,
scopes=scopes)
self.assertIsInstance(creds, ServiceAccountCredentials)
self.assertEqual(creds.client_id, None)
self.assertEqual(creds._service_account_email, service_account_email)
self.assertEqual(creds._private_key_id, None)
self.assertEqual(creds._private_key_pkcs8_pem, None)
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))

def test_from_p12_keyfile_defaults(self):
self._from_p12_keyfile_helper()

def test_from_p12_keyfile_explicit(self):
password = 'notasecret'
self._from_p12_keyfile_helper(private_key_password=password,
scopes=['foo', 'bar'])

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

Expand Down

0 comments on commit 4418564

Please sign in to comment.