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

Commit

Permalink
Removing novel crypto in service_account.py.
Browse files Browse the repository at this point in the history
  • Loading branch information
dhermes committed Feb 4, 2016
1 parent 4f4b7de commit af3e689
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 79 deletions.
7 changes: 5 additions & 2 deletions oauth2client/crypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,27 +71,30 @@ def _bad_pkcs12_key_as_pem(*args, **kwargs):
Verifier = RsaVerifier


def make_signed_jwt(signer, payload):
def make_signed_jwt(signer, payload, key_id=None):
"""Make a signed JWT.
See http://self-issued.info/docs/draft-jones-json-web-token.html.
Args:
signer: crypt.Signer, Cryptographic signer.
payload: dict, Dictionary of data to convert to JSON and then sign.
key_id: string, (Optional) Key ID header.
Returns:
string, The JWT for the payload.
"""
header = {'typ': 'JWT', 'alg': 'RS256'}
if key_id is not None:
header['kid'] = key_id

segments = [
_urlsafe_b64encode(_json_encode(header)),
_urlsafe_b64encode(_json_encode(payload)),
]
signing_input = b'.'.join(segments)

signature = signer.sign(signing_input)
signature = signer.sign(signing_input).rstrip(b'=')
segments.append(_urlsafe_b64encode(signature))

logger.debug(str(segments))
Expand Down
54 changes: 9 additions & 45 deletions oauth2client/service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""A service account credentials class.
This credentials class is implemented on top of rsa library.
"""
"""oauth2client Service account credentials class."""

import base64
import datetime
import json
import time

from pyasn1.codec.ber import decoder
from pyasn1_modules.rfc5208 import PrivateKeyInfo
import rsa

from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client._helpers import _json_encode
Expand All @@ -35,6 +28,7 @@
from oauth2client import util
from oauth2client.client import AssertionCredentials
from oauth2client.client import EXPIRY_FORMAT
from oauth2client import crypt


class _ServiceAccountCredentials(AssertionCredentials):
Expand All @@ -43,10 +37,9 @@ class _ServiceAccountCredentials(AssertionCredentials):
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds

NON_SERIALIZED_MEMBERS = (
frozenset(['_private_key']) |
frozenset(['_signer']) |
AssertionCredentials.NON_SERIALIZED_MEMBERS)


def __init__(self, service_account_id, service_account_email,
private_key_id, private_key_pkcs8_text, scopes,
user_agent=None, token_uri=GOOGLE_TOKEN_URI,
Expand All @@ -59,8 +52,8 @@ def __init__(self, service_account_id, service_account_email,
self._service_account_id = service_account_id
self._service_account_email = service_account_email
self._private_key_id = private_key_id
self._private_key = _get_private_key(private_key_pkcs8_text)
self._private_key_pkcs8_text = private_key_pkcs8_text
self._signer = crypt.Signer.from_string(self._private_key_pkcs8_text)
self._scopes = util.scopes_to_string(scopes)
self._user_agent = user_agent
self._token_uri = token_uri
Expand All @@ -69,39 +62,20 @@ def __init__(self, service_account_id, service_account_email,

def _generate_assertion(self):
"""Generate the assertion that will be used in the request."""

header = {
'alg': 'RS256',
'typ': 'JWT',
'kid': self._private_key_id
}

now = int(time.time())
payload = {
'aud': self._token_uri,
'scope': self._scopes,
'iat': now,
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
'iss': self._service_account_email
'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
'iss': self._service_account_email,
}
payload.update(self._kwargs)

first_segment = _urlsafe_b64encode(_json_encode(header))
second_segment = _urlsafe_b64encode(_json_encode(payload))
assertion_input = first_segment + b'.' + second_segment

# Sign the assertion.
rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key,
'SHA-256')
signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')

return assertion_input + b'.' + signature
return crypt.make_signed_jwt(self._signer, payload,
key_id=self._private_key_id)

def sign_blob(self, blob):
# Ensure that it is bytes
blob = _to_bytes(blob, encoding='utf-8')
return (self._private_key_id,
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
return self._private_key_id, self._signer.sign(blob)

@property
def service_account_email(self):
Expand Down Expand Up @@ -149,13 +123,3 @@ def create_scoped(self, scopes):
token_uri=self._token_uri,
revoke_uri=self._revoke_uri,
**self._kwargs)


def _get_private_key(private_key_pkcs8_text):
"""Get an RSA private key object from a pkcs8 representation."""
private_key_pkcs8_text = _to_bytes(private_key_pkcs8_text)
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
return rsa.PrivateKey.load_pkcs1(
asn1_private_key.getComponentByName('privateKey').asOctets(),
format='DER')
16 changes: 9 additions & 7 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,15 +628,17 @@ def test_to_from_json_authorized_user(self):
self.assertEqual(creds.__dict__, creds2.__dict__)

def test_to_from_json_service_account(self):
self.maxDiff=None
credentials_file = datafile(
os.path.join('gcloud', _WELL_KNOWN_CREDENTIALS_FILE))
creds = GoogleCredentials.from_stream(credentials_file)

json = creds.to_json()
creds2 = GoogleCredentials.from_json(json)

self.assertEqual(creds.__dict__, creds2.__dict__)
creds1 = GoogleCredentials.from_stream(credentials_file)
# Convert to and then back from json.
creds2 = GoogleCredentials.from_json(creds1.to_json())

creds1_vals = creds1.__dict__
creds1_vals.pop('_signer')
creds2_vals = creds2.__dict__
creds2_vals.pop('_signer')
self.assertEqual(creds1_vals, creds2_vals)

def test_parse_expiry(self):
dt = datetime.datetime(2016, 1, 1)
Expand Down
66 changes: 41 additions & 25 deletions tests/test_service_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,29 @@ def test_create_scoped(self):
self.assertEqual('dummy_scope', new_credentials._scopes)

@mock.patch('oauth2client.client._UTCNOW')
@mock.patch('rsa.pkcs1.sign', return_value=b'signed-value')
def test_access_token(self, sign_func, utcnow):
def test_access_token(self, utcnow):
# Configure the patch.
seconds = 11
NOW = datetime.datetime(1992, 12, 31, second=seconds)
utcnow.return_value = NOW

# Create a custom credentials with a mock signer.
signer = mock.MagicMock()
signed_value = b'signed-content'
signer.sign = mock.MagicMock(name='sign',
return_value=signed_value)
signer_patch = mock.patch('oauth2client.crypt.Signer.from_string',
return_value=signer)
with signer_patch as signer_factory:
credentials = _ServiceAccountCredentials(
self.service_account_id,
self.service_account_email,
self.private_key_id,
self.private_key,
'',
)

# Begin testing.
lifetime = 2 # number of seconds in which the token expires
EXPIRY_TIME = datetime.datetime(1992, 12, 31,
second=seconds + lifetime)
Expand All @@ -120,51 +136,51 @@ def test_access_token(self, sign_func, utcnow):
])

# Get Access Token, First attempt.
self.assertEqual(self.credentials.access_token, None)
self.assertFalse(self.credentials.access_token_expired)
self.assertEqual(self.credentials.token_expiry, None)
token = self.credentials.get_access_token(http=http)
self.assertEqual(self.credentials.token_expiry, EXPIRY_TIME)
self.assertEqual(credentials.access_token, None)
self.assertFalse(credentials.access_token_expired)
self.assertEqual(credentials.token_expiry, None)
token = credentials.get_access_token(http=http)
self.assertEqual(credentials.token_expiry, EXPIRY_TIME)
self.assertEqual(token1, token.access_token)
self.assertEqual(lifetime, token.expires_in)
self.assertEqual(token_response_first,
self.credentials.token_response)
credentials.token_response)
# Two utcnow calls are expected:
# - get_access_token() -> _do_refresh_request (setting expires in)
# - get_access_token() -> _expires_in()
expected_utcnow_calls = [mock.call()] * 2
self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
# One rsa.pkcs1.sign expected: Actual refresh was needed.
self.assertEqual(len(sign_func.mock_calls), 1)
# One call to sign() expected: Actual refresh was needed.
self.assertEqual(len(signer.sign.mock_calls), 1)

# Get Access Token, Second Attempt (not expired)
self.assertEqual(self.credentials.access_token, token1)
self.assertFalse(self.credentials.access_token_expired)
token = self.credentials.get_access_token(http=http)
self.assertEqual(credentials.access_token, token1)
self.assertFalse(credentials.access_token_expired)
token = credentials.get_access_token(http=http)
# Make sure no refresh occurred since the token was not expired.
self.assertEqual(token1, token.access_token)
self.assertEqual(lifetime, token.expires_in)
self.assertEqual(token_response_first, self.credentials.token_response)
self.assertEqual(token_response_first, credentials.token_response)
# Three more utcnow calls are expected:
# - access_token_expired
# - get_access_token() -> access_token_expired
# - get_access_token -> _expires_in
expected_utcnow_calls = [mock.call()] * (2 + 3)
self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
# No rsa.pkcs1.sign expected: the token was not expired.
self.assertEqual(len(sign_func.mock_calls), 1 + 0)
# No call to sign() expected: the token was not expired.
self.assertEqual(len(signer.sign.mock_calls), 1 + 0)

# Get Access Token, Third Attempt (force expiration)
self.assertEqual(self.credentials.access_token, token1)
self.credentials.token_expiry = NOW # Manually force expiry.
self.assertTrue(self.credentials.access_token_expired)
token = self.credentials.get_access_token(http=http)
self.assertEqual(credentials.access_token, token1)
credentials.token_expiry = NOW # Manually force expiry.
self.assertTrue(credentials.access_token_expired)
token = credentials.get_access_token(http=http)
# Make sure refresh occurred since the token was not expired.
self.assertEqual(token2, token.access_token)
self.assertEqual(lifetime, token.expires_in)
self.assertFalse(self.credentials.access_token_expired)
self.assertFalse(credentials.access_token_expired)
self.assertEqual(token_response_second,
self.credentials.token_response)
credentials.token_response)
# Five more utcnow calls are expected:
# - access_token_expired
# - get_access_token -> access_token_expired
Expand All @@ -173,10 +189,10 @@ def test_access_token(self, sign_func, utcnow):
# - access_token_expired
expected_utcnow_calls = [mock.call()] * (2 + 3 + 5)
self.assertEqual(expected_utcnow_calls, utcnow.mock_calls)
# One more rsa.pkcs1.sign expected: Actual refresh was needed.
self.assertEqual(len(sign_func.mock_calls), 1 + 0 + 1)
# One more call to sign() expected: Actual refresh was needed.
self.assertEqual(len(signer.sign.mock_calls), 1 + 0 + 1)

self.assertEqual(self.credentials.access_token, token2)
self.assertEqual(credentials.access_token, token2)


if __name__ == '__main__': # pragma: NO COVER
Expand Down

0 comments on commit af3e689

Please sign in to comment.