Skip to content

Commit 8b4c173

Browse files
committed
[mq]: id_token2
1 parent 66f5752 commit 8b4c173

14 files changed

+777
-65
lines changed

Makefile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
pep8:
22
find apiclient samples -name "*.py" | xargs pep8 --ignore=E111,E202
33

4+
APP_ENGINE_PATH=../google_appengine
5+
46
test:
5-
python runtests.py tests
7+
python runtests.py tests/test_discovery.py
8+
python runtests.py tests/test_errors.py
9+
python runtests.py tests/test_http.py
10+
python runtests.py tests/test_json_model.py
11+
python runtests.py tests/test_mocks.py
12+
python runtests.py tests/test_model.py
13+
python runtests.py tests/test_oauth2client_clientsecrets.py
14+
python runtests.py tests/test_oauth2client_django_orm.py
15+
python runtests.py tests/test_oauth2client_file.py
16+
python runtests.py tests/test_oauth2client_jwt.py
17+
python runtests.py tests/test_oauth2client.py
18+
python runtests.py tests/test_oauth.py
19+
python runtests.py tests/test_protobuf_model.py
20+
python runtests.py tests/test_oauth2client_appengine.py
621

722
.PHONY: docs
823
docs:

oauth2client/client.py

Lines changed: 182 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,28 @@
1919

2020
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
2121

22+
import base64
2223
import clientsecrets
2324
import copy
2425
import datetime
2526
import httplib2
2627
import logging
28+
import os
2729
import sys
30+
import time
2831
import urllib
2932
import urlparse
3033

34+
35+
HAS_OPENSSL = False
36+
try:
37+
from oauth2client.crypt import Signer
38+
from oauth2client.crypt import make_signed_jwt
39+
from oauth2client.crypt import verify_signed_jwt_with_certs
40+
HAS_OPENSSL = True
41+
except ImportError:
42+
pass
43+
3144
try: # pragma: no cover
3245
import simplejson
3346
except ImportError: # pragma: no cover
@@ -43,10 +56,21 @@
4356
except ImportError:
4457
from cgi import parse_qsl
4558

59+
# Determine if we can write to the file system, and if we can use a local file
60+
# cache behing httplib2.
61+
if hasattr(os, 'tempnam'):
62+
# Put cache file in the director '.cache'.
63+
CACHED_HTTP = httplib2.Http('.cache')
64+
else:
65+
CACHED_HTTP = httplib2.Http()
66+
4667
logger = logging.getLogger(__name__)
4768

4869
# Expiry is stored in RFC3339 UTC format
49-
EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
70+
EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
71+
72+
# Which certs to use to validate id_tokens received.
73+
ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
5074

5175

5276
class Error(Exception):
@@ -73,6 +97,11 @@ class AccessTokenCredentialsError(Error):
7397
pass
7498

7599

100+
class VerifyJwtTokenError(Error):
101+
"""Could on retrieve certificates for validation."""
102+
pass
103+
104+
76105
def _abstract():
77106
raise NotImplementedError('You need to override this function')
78107

@@ -229,14 +258,13 @@ class OAuth2Credentials(Credentials):
229258
"""Credentials object for OAuth 2.0.
230259
231260
Credentials can be applied to an httplib2.Http object using the authorize()
232-
method, which then signs each request from that object with the OAuth 2.0
233-
access token.
261+
method, which then adds the OAuth 2.0 access token to each request.
234262
235263
OAuth2Credentials objects may be safely pickled and unpickled.
236264
"""
237265

238266
def __init__(self, access_token, client_id, client_secret, refresh_token,
239-
token_expiry, token_uri, user_agent):
267+
token_expiry, token_uri, user_agent, id_token=None):
240268
"""Create an instance of OAuth2Credentials.
241269
242270
This constructor is not usually called by the user, instead
@@ -250,9 +278,10 @@ def __init__(self, access_token, client_id, client_secret, refresh_token,
250278
token_expiry: datetime, when the access_token expires.
251279
token_uri: string, URI of token endpoint.
252280
user_agent: string, The HTTP User-Agent to provide for this application.
281+
id_token: object, The identity of the resource owner.
253282
254283
Notes:
255-
store: callable, a callable that when passed a Credential
284+
store: callable, A callable that when passed a Credential
256285
will store the credential back to where it came from.
257286
This is needed to store the latest access_token if it
258287
has expired and been refreshed.
@@ -265,6 +294,7 @@ def __init__(self, access_token, client_id, client_secret, refresh_token,
265294
self.token_expiry = token_expiry
266295
self.token_uri = token_uri
267296
self.user_agent = user_agent
297+
self.id_token = id_token
268298

269299
# True if the credentials have been revoked or expired and can't be
270300
# refreshed.
@@ -299,7 +329,8 @@ def from_json(cls, s):
299329
data['refresh_token'],
300330
data['token_expiry'],
301331
data['token_uri'],
302-
data['user_agent'])
332+
data['user_agent'],
333+
data.get('id_token', None))
303334
retval.invalid = data['invalid']
304335
return retval
305336

@@ -607,6 +638,145 @@ def _generate_assertion(self):
607638
"""
608639
_abstract()
609640

641+
if HAS_OPENSSL:
642+
# PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
643+
# don't create the SignedJwtAssertionCredentials or the verify_id_token()
644+
# method.
645+
646+
class SignedJwtAssertionCredentials(AssertionCredentials):
647+
"""Credentials object used for OAuth 2.0 Signed JWT assertion grants.
648+
649+
This credential does not require a flow to instantiate because it
650+
represents a two legged flow, and therefore has all of the required
651+
information to generate and refresh its own access tokens.
652+
"""
653+
654+
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
655+
656+
def __init__(self,
657+
service_account_name,
658+
private_key,
659+
scope,
660+
private_key_password='notasecret',
661+
user_agent=None,
662+
token_uri='https://accounts.google.com/o/oauth2/token',
663+
**kwargs):
664+
"""Constructor for SignedJwtAssertionCredentials.
665+
666+
Args:
667+
service_account_name: string, id for account, usually an email address.
668+
private_key: string, private key in P12 format.
669+
scope: string or list of strings, scope(s) of the credentials being
670+
requested.
671+
private_key_password: string, password for private_key.
672+
user_agent: string, HTTP User-Agent to provide for this application.
673+
token_uri: string, URI for token endpoint. For convenience
674+
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
675+
kwargs: kwargs, Additional parameters to add to the JWT token, for
676+
example prn=joe@xample.org."""
677+
678+
super(SignedJwtAssertionCredentials, self).__init__(
679+
'http://oauth.net/grant_type/jwt/1.0/bearer',
680+
user_agent,
681+
token_uri=token_uri,
682+
)
683+
684+
if type(scope) is list:
685+
scope = ' '.join(scope)
686+
self.scope = scope
687+
688+
self.private_key = private_key
689+
self.private_key_password = private_key_password
690+
self.service_account_name = service_account_name
691+
self.kwargs = kwargs
692+
693+
@classmethod
694+
def from_json(cls, s):
695+
data = simplejson.loads(s)
696+
retval = SignedJwtAssertionCredentials(
697+
data['service_account_name'],
698+
data['private_key'],
699+
data['private_key_password'],
700+
data['scope'],
701+
data['user_agent'],
702+
data['token_uri'],
703+
data['kwargs']
704+
)
705+
retval.invalid = data['invalid']
706+
return retval
707+
708+
def _generate_assertion(self):
709+
"""Generate the assertion that will be used in the request."""
710+
now = long(time.time())
711+
payload = {
712+
'aud': self.token_uri,
713+
'scope': self.scope,
714+
'iat': now,
715+
'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
716+
'iss': self.service_account_name
717+
}
718+
payload.update(self.kwargs)
719+
logging.debug(str(payload))
720+
721+
return make_signed_jwt(
722+
Signer.from_string(self.private_key, self.private_key_password),
723+
payload)
724+
725+
726+
def verify_id_token(id_token, audience, http=None,
727+
cert_uri=ID_TOKEN_VERIFICATON_CERTS):
728+
"""Verifies a signed JWT id_token.
729+
730+
Args:
731+
id_token: string, A Signed JWT.
732+
audience: string, The audience 'aud' that the token should be for.
733+
http: httplib2.Http, instance to use to make the HTTP request. Callers
734+
should supply an instance that has caching enabled.
735+
cert_uri: string, URI of the certificates in JSON format to
736+
verify the JWT against.
737+
738+
Returns:
739+
The deserialized JSON in the JWT.
740+
741+
Raises:
742+
oauth2client.crypt.AppIdentityError if the JWT fails to verify.
743+
"""
744+
if http is None:
745+
http = CACHED_HTTP
746+
747+
resp, content = http.request(cert_uri)
748+
749+
if resp.status == 200:
750+
certs = simplejson.loads(content)
751+
return verify_signed_jwt_with_certs(id_token, certs, audience)
752+
else:
753+
raise VerifyJwtTokenError('Status code: %d' % resp.status)
754+
755+
756+
def _urlsafe_b64decode(b64string):
757+
padded = b64string + '=' * (4 - len(b64string) % 4)
758+
return base64.urlsafe_b64decode(padded)
759+
760+
761+
def _extract_id_token(id_token):
762+
"""Extract the JSON payload from a JWT.
763+
764+
Does the extraction w/o checking the signature.
765+
766+
Args:
767+
id_token: string, OAuth 2.0 id_token.
768+
769+
Returns:
770+
object, The deserialized JSON payload.
771+
"""
772+
segments = id_token.split('.')
773+
774+
if (len(segments) != 3):
775+
raise VerifyJwtTokenError(
776+
'Wrong number of segments in token: %s' % id_token)
777+
778+
return simplejson.loads(_urlsafe_b64decode(segments[1]))
779+
610780

611781
class OAuth2WebServerFlow(Flow):
612782
"""Does the Web Server Flow for OAuth 2.0.
@@ -704,6 +874,7 @@ def step2_exchange(self, code, http=None):
704874

705875
if http is None:
706876
http = httplib2.Http()
877+
707878
resp, content = http.request(self.token_uri, method='POST', body=body,
708879
headers=headers)
709880
if resp.status == 200:
@@ -716,10 +887,14 @@ def step2_exchange(self, code, http=None):
716887
token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
717888
seconds=int(d['expires_in']))
718889

890+
if 'id_token' in d:
891+
d['id_token'] = _extract_id_token(d['id_token'])
892+
719893
logger.info('Successfully retrieved access token: %s' % content)
720894
return OAuth2Credentials(access_token, self.client_id,
721895
self.client_secret, refresh_token, token_expiry,
722-
self.token_uri, self.user_agent)
896+
self.token_uri, self.user_agent,
897+
id_token=d.get('id_token', None))
723898
else:
724899
logger.error('Failed to retrieve access token: %s' % content)
725900
error_msg = 'Invalid response %s.' % resp['status']

0 commit comments

Comments
 (0)