1919
2020__author__ = 'jcgregorio@google.com (Joe Gregorio)'
2121
22+ import base64
2223import clientsecrets
2324import copy
2425import datetime
2526import httplib2
2627import logging
28+ import os
2729import sys
30+ import time
2831import urllib
2932import 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+
3144try : # pragma: no cover
3245 import simplejson
3346except ImportError : # pragma: no cover
4356except 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+
4667logger = 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
5276class 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+
76105def _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
611781class 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