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

Commit dcd20c9

Browse files
dhermesnathanielmanistaatgoogle
authored andcommitted
Removing SignedJwtAssertionCredentials.
This completes the consolidation of the two service account credentials implementations. In the process, also adding test coverage for some untested code paths within the crypto helpers.
1 parent d3391bc commit dcd20c9

File tree

8 files changed

+118
-204
lines changed

8 files changed

+118
-204
lines changed

oauth2client/_openssl_crypt.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,17 @@ def from_string(key, password=b'notasecret'):
123123
return OpenSSLSigner(pkey)
124124

125125

126-
def pkcs12_key_as_pem(private_key_text, private_key_password):
127-
"""Convert the contents of a PKCS12 key to PEM using OpenSSL.
126+
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
127+
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
128128
129129
Args:
130-
private_key_text: String. Private key.
131-
private_key_password: String. Password for PKCS12.
130+
private_key_bytes: Bytes. PKCS#12 key in DER format.
131+
private_key_password: String. Password for PKCS#12 key.
132132
133133
Returns:
134-
String. PEM contents of ``private_key_text``.
134+
String. PEM contents of ``private_key_bytes``.
135135
"""
136-
decoded_body = base64.b64decode(private_key_text)
137136
private_key_password = _to_bytes(private_key_password)
138-
139-
pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
137+
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
140138
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
141139
pkcs12.get_privatekey())

oauth2client/client.py

Lines changed: 11 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -298,20 +298,20 @@ def to_json(self):
298298
return self._to_json(self.NON_SERIALIZED_MEMBERS)
299299

300300
@classmethod
301-
def new_from_json(cls, s):
301+
def new_from_json(cls, json_data):
302302
"""Utility class method to instantiate a Credentials subclass from JSON.
303303
304304
Expects the JSON string to have been produced by to_json().
305305
306306
Args:
307-
s: string or bytes, JSON from to_json().
307+
json_data: string or bytes, JSON from to_json().
308308
309309
Returns:
310310
An instance of the subclass of Credentials that was serialized with
311311
to_json().
312312
"""
313-
json_string_as_unicode = _from_bytes(s)
314-
data = json.loads(json_string_as_unicode)
313+
json_data_as_unicode = _from_bytes(json_data)
314+
data = json.loads(json_data_as_unicode)
315315
# Find and call the right classmethod from_json() to restore
316316
# the object.
317317
module_name = data['_module']
@@ -326,8 +326,7 @@ def new_from_json(cls, s):
326326
module_obj = __import__(module_name,
327327
fromlist=module_name.split('.')[:-1])
328328
kls = getattr(module_obj, data['_class'])
329-
from_json = getattr(kls, 'from_json')
330-
return from_json(json_string_as_unicode)
329+
return kls.from_json(json_data_as_unicode)
331330

332331
@classmethod
333332
def from_json(cls, unused_data):
@@ -710,19 +709,18 @@ def retrieve_scopes(self, http):
710709
return self.scopes
711710

712711
@classmethod
713-
def from_json(cls, s):
712+
def from_json(cls, json_data):
714713
"""Instantiate a Credentials object from a JSON description of it.
715714
716715
The JSON should have been produced by calling .to_json() on the object.
717716
718717
Args:
719-
data: dict, A deserialized JSON object.
718+
json_data: string or bytes, JSON to deserialize.
720719
721720
Returns:
722721
An instance of a Credentials subclass.
723722
"""
724-
s = _from_bytes(s)
725-
data = json.loads(s)
723+
data = json.loads(_from_bytes(json_data))
726724
if (data.get('token_expiry') and
727725
not isinstance(data['token_expiry'], datetime.datetime)):
728726
try:
@@ -1070,8 +1068,8 @@ def __init__(self, access_token, user_agent, revoke_uri=None):
10701068
revoke_uri=revoke_uri)
10711069

10721070
@classmethod
1073-
def from_json(cls, s):
1074-
data = json.loads(_from_bytes(s))
1071+
def from_json(cls, json_data):
1072+
data = json.loads(_from_bytes(json_data))
10751073
retval = AccessTokenCredentials(
10761074
data['access_token'],
10771075
data['user_agent'])
@@ -1190,7 +1188,7 @@ class GoogleCredentials(OAuth2Credentials):
11901188
NON_SERIALIZED_MEMBERS = (
11911189
frozenset(['_private_key']) |
11921190
OAuth2Credentials.NON_SERIALIZED_MEMBERS)
1193-
1191+
"""Members that aren't serialized when object is converted to JSON."""
11941192

11951193
def __init__(self, access_token, client_id, client_secret, refresh_token,
11961194
token_expiry, token_uri, user_agent,
@@ -1630,101 +1628,6 @@ def _RequireCryptoOrDie():
16301628
raise CryptoUnavailableError('No crypto library available')
16311629

16321630

1633-
class SignedJwtAssertionCredentials(AssertionCredentials):
1634-
"""Credentials object used for OAuth 2.0 Signed JWT assertion grants.
1635-
1636-
This credential does not require a flow to instantiate because it
1637-
represents a two legged flow, and therefore has all of the required
1638-
information to generate and refresh its own access tokens.
1639-
1640-
SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto
1641-
2.6 or later. For App Engine you may also consider using
1642-
AppAssertionCredentials.
1643-
"""
1644-
1645-
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
1646-
1647-
@util.positional(4)
1648-
def __init__(self,
1649-
service_account_name,
1650-
private_key,
1651-
scope,
1652-
private_key_password='notasecret',
1653-
user_agent=None,
1654-
token_uri=GOOGLE_TOKEN_URI,
1655-
revoke_uri=GOOGLE_REVOKE_URI,
1656-
**kwargs):
1657-
"""Constructor for SignedJwtAssertionCredentials.
1658-
1659-
Args:
1660-
service_account_name: string, id for account, usually an email
1661-
address.
1662-
private_key: string or bytes, private key in PKCS12 or PEM format.
1663-
scope: string or iterable of strings, scope(s) of the credentials
1664-
being requested.
1665-
private_key_password: string, password for private_key, unused if
1666-
private_key is in PEM format.
1667-
user_agent: string, HTTP User-Agent to provide for this
1668-
application.
1669-
token_uri: string, URI for token endpoint. For convenience defaults
1670-
to Google's endpoints but any OAuth 2.0 provider can be
1671-
used.
1672-
revoke_uri: string, URI for revoke endpoint.
1673-
kwargs: kwargs, Additional parameters to add to the JWT token, for
1674-
example sub=joe@xample.org.
1675-
1676-
Raises:
1677-
CryptoUnavailableError if no crypto library is available.
1678-
"""
1679-
_RequireCryptoOrDie()
1680-
super(SignedJwtAssertionCredentials, self).__init__(
1681-
None,
1682-
user_agent=user_agent,
1683-
token_uri=token_uri,
1684-
revoke_uri=revoke_uri,
1685-
)
1686-
1687-
self.scope = util.scopes_to_string(scope)
1688-
1689-
# Keep base64 encoded so it can be stored in JSON.
1690-
self.private_key = base64.b64encode(_to_bytes(private_key))
1691-
self.private_key_password = private_key_password
1692-
self.service_account_name = service_account_name
1693-
self.kwargs = kwargs
1694-
1695-
@classmethod
1696-
def from_json(cls, s):
1697-
data = json.loads(_from_bytes(s))
1698-
retval = SignedJwtAssertionCredentials(
1699-
data['service_account_name'],
1700-
base64.b64decode(data['private_key']),
1701-
data['scope'],
1702-
private_key_password=data['private_key_password'],
1703-
user_agent=data['user_agent'],
1704-
token_uri=data['token_uri'],
1705-
**data['kwargs']
1706-
)
1707-
retval.invalid = data['invalid']
1708-
retval.access_token = data['access_token']
1709-
return retval
1710-
1711-
def _generate_assertion(self):
1712-
"""Generate the assertion that will be used in the request."""
1713-
now = int(time.time())
1714-
payload = {
1715-
'aud': self.token_uri,
1716-
'scope': self.scope,
1717-
'iat': now,
1718-
'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
1719-
'iss': self.service_account_name
1720-
}
1721-
payload.update(self.kwargs)
1722-
logger.debug(str(payload))
1723-
1724-
private_key = base64.b64decode(self.private_key)
1725-
return crypt.make_signed_jwt(crypt.Signer.from_string(
1726-
private_key, self.private_key_password), payload)
1727-
17281631
# Only used in verify_id_token(), which is always calling to the same URI
17291632
# for the certs.
17301633
_cached_http = httplib2.Http(MemoryCache())

oauth2client/service_account.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,7 @@ def from_json(cls, json_data):
308308
# state.
309309
pkcs12_val = base64.b64decode(pkcs12_val)
310310
password = json_data['_private_key_password']
311-
signer = crypt.Signer.from_string(private_key_pkcs12,
312-
private_key_password)
311+
signer = crypt.Signer.from_string(pkcs12_val, password)
313312

314313
credentials = cls(
315314
json_data['_service_account_email'],

tests/test__pure_python_crypt.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ def test_from_string_pkcs12(self):
174174
with self.assertRaises(ValueError):
175175
RsaSigner.from_string(key_bytes)
176176

177+
def test_from_string_bogus_key(self):
178+
key_bytes = 'bogus-key'
179+
with self.assertRaises(ValueError):
180+
RsaSigner.from_string(key_bytes)
181+
177182

178183
if __name__ == '__main__': # pragma: NO COVER
179184
unittest2.main()

tests/test__pycrypto_crypt.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
"""Unit tests for oauth2client._pycrypto_crypt."""
1515

1616
import os
17-
import unittest
17+
import unittest2
1818

1919
from oauth2client.crypt import PyCryptoSigner
2020
from oauth2client.crypt import PyCryptoVerifier
2121

2222

23-
class TestPyCryptoVerifier(unittest.TestCase):
23+
class TestPyCryptoVerifier(unittest2.TestCase):
2424

2525
PUBLIC_CERT_FILENAME = os.path.join(os.path.dirname(__file__),
2626
'data', 'public_cert.pem')
@@ -63,5 +63,13 @@ def test_from_string_unicode_key(self):
6363
self.assertTrue(isinstance(verifier, PyCryptoVerifier))
6464

6565

66+
class TestPyCryptoSigner(unittest2.TestCase):
67+
68+
def test_from_string_bad_key(self):
69+
key_bytes = 'definitely-not-pem-format'
70+
with self.assertRaises(NotImplementedError):
71+
PyCryptoSigner.from_string(key_bytes)
72+
73+
6674
if __name__ == '__main__': # pragma: NO COVER
67-
unittest.main()
75+
unittest2.main()

tests/test_crypt.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@
2020

2121
from oauth2client import _helpers
2222
from oauth2client.client import HAS_OPENSSL
23-
from oauth2client.client import SignedJwtAssertionCredentials
2423
from oauth2client import crypt
24+
from oauth2client.service_account import ServiceAccountCredentials
25+
26+
27+
def data_filename(filename):
28+
return os.path.join(os.path.dirname(__file__), 'data', filename)
2529

2630

2731
def datafile(filename):
28-
f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'rb')
29-
data = f.read()
30-
f.close()
31-
return data
32+
with open(data_filename(filename), 'rb') as file_obj:
33+
return file_obj.read()
3234

3335

3436
class Test__bad_pkcs12_key_as_pem(unittest.TestCase):
@@ -39,23 +41,23 @@ def test_fails(self):
3941

4042
class Test_pkcs12_key_as_pem(unittest.TestCase):
4143

42-
def _make_signed_jwt_creds(self, private_key_file='privatekey.p12',
43-
private_key=None):
44-
private_key = private_key or datafile(private_key_file)
45-
return SignedJwtAssertionCredentials(
44+
def _make_svc_account_creds(self, private_key_file='privatekey.p12'):
45+
filename = data_filename(private_key_file)
46+
credentials = ServiceAccountCredentials.from_p12_keyfile(
4647
'some_account@example.com',
47-
private_key,
48-
scope='read+write',
49-
sub='joe@example.org')
48+
filename,
49+
scopes='read+write')
50+
credentials._kwargs['sub'] ='joe@example.org'
51+
return credentials
5052

5153
def _succeeds_helper(self, password=None):
5254
self.assertEqual(True, HAS_OPENSSL)
5355

54-
credentials = self._make_signed_jwt_creds()
56+
credentials = self._make_svc_account_creds()
5557
if password is None:
56-
password = credentials.private_key_password
57-
pem_contents = crypt.pkcs12_key_as_pem(credentials.private_key,
58-
password)
58+
password = credentials._private_key_password
59+
pem_contents = crypt.pkcs12_key_as_pem(
60+
credentials._private_key_pkcs12, password)
5961
pkcs12_key_as_pem = datafile('pem_from_pkcs12.pem')
6062
pkcs12_key_as_pem = _helpers._parse_pem_key(pkcs12_key_as_pem)
6163
alternate_pem = datafile('pem_from_pkcs12_alternate.pem')
@@ -68,13 +70,6 @@ def test_succeeds_with_unicode_password(self):
6870
password = u'notasecret'
6971
self._succeeds_helper(password)
7072

71-
def test_with_nonsense_key(self):
72-
from OpenSSL import crypto
73-
credentials = self._make_signed_jwt_creds(private_key=b'NOT_A_KEY')
74-
self.assertRaises(crypto.Error, crypt.pkcs12_key_as_pem,
75-
credentials.private_key,
76-
credentials.private_key_password)
77-
7873

7974
class Test__verify_signature(unittest.TestCase):
8075

0 commit comments

Comments
 (0)