1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """An OAuth 2.0 client.
16
17 Tools for interacting with OAuth 2.0 protected resources.
18 """
19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
22 import base64
23 import clientsecrets
24 import copy
25 import datetime
26 import httplib2
27 import logging
28 import os
29 import sys
30 import time
31 import urllib
32 import urlparse
33
34 from oauth2client import util
35 from oauth2client.anyjson import simplejson
36
37 HAS_CRYPTO = False
38 try:
39 from oauth2client import crypt
40 HAS_CRYPTO = True
41 except ImportError:
42 pass
43
44 try:
45 from urlparse import parse_qsl
46 except ImportError:
47 from cgi import parse_qsl
48
49 logger = logging.getLogger(__name__)
50
51
52 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
53
54
55 ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
56
57
58 OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
59
60
61 REFRESH_STATUS_CODES = [401]
62
63
64 -class Error(Exception):
65 """Base error for this module."""
66 pass
67
70 """Error trying to exchange an authorization grant for an access token."""
71 pass
72
75 """Error trying to refresh an expired access token."""
76 pass
77
79 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
80 pass
81
84 """Having only the access_token means no refresh is possible."""
85 pass
86
89 """Could on retrieve certificates for validation."""
90 pass
91
94 """Header names and values must be ASCII strings."""
95 pass
96
99 raise NotImplementedError('You need to override this function')
100
103 """httplib2 Cache implementation which only caches locally."""
104
107
108 - def get(self, key):
109 return self.cache.get(key)
110
111 - def set(self, key, value):
112 self.cache[key] = value
113
115 self.cache.pop(key, None)
116
119 """Base class for all Credentials objects.
120
121 Subclasses must define an authorize() method that applies the credentials to
122 an HTTP transport.
123
124 Subclasses must also specify a classmethod named 'from_json' that takes a JSON
125 string as input and returns an instaniated Credentials object.
126 """
127
128 NON_SERIALIZED_MEMBERS = ['store']
129
131 """Take an httplib2.Http instance (or equivalent) and
132 authorizes it for the set of credentials, usually by
133 replacing http.request() with a method that adds in
134 the appropriate headers and then delegates to the original
135 Http.request() method.
136 """
137 _abstract()
138
140 """Forces a refresh of the access_token.
141
142 Args:
143 http: httplib2.Http, an http object to be used to make the refresh
144 request.
145 """
146 _abstract()
147
148 - def apply(self, headers):
149 """Add the authorization to the headers.
150
151 Args:
152 headers: dict, the headers to add the Authorization header to.
153 """
154 _abstract()
155
157 """Utility function for creating a JSON representation of an instance of Credentials.
158
159 Args:
160 strip: array, An array of names of members to not include in the JSON.
161
162 Returns:
163 string, a JSON representation of this instance, suitable to pass to
164 from_json().
165 """
166 t = type(self)
167 d = copy.copy(self.__dict__)
168 for member in strip:
169 if member in d:
170 del d[member]
171 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
172 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
173
174 d['_class'] = t.__name__
175 d['_module'] = t.__module__
176 return simplejson.dumps(d)
177
179 """Creating a JSON representation of an instance of Credentials.
180
181 Returns:
182 string, a JSON representation of this instance, suitable to pass to
183 from_json().
184 """
185 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
186
187 @classmethod
189 """Utility class method to instantiate a Credentials subclass from a JSON
190 representation produced by to_json().
191
192 Args:
193 s: string, JSON from to_json().
194
195 Returns:
196 An instance of the subclass of Credentials that was serialized with
197 to_json().
198 """
199 data = simplejson.loads(s)
200
201 module = data['_module']
202 try:
203 m = __import__(module)
204 except ImportError:
205
206 module = module.replace('.apiclient', '')
207 m = __import__(module)
208
209 m = __import__(module, fromlist=module.split('.')[:-1])
210 kls = getattr(m, data['_class'])
211 from_json = getattr(kls, 'from_json')
212 return from_json(s)
213
214 @classmethod
216 """Instantiate a Credentials object from a JSON description of it.
217
218 The JSON should have been produced by calling .to_json() on the object.
219
220 Args:
221 data: dict, A deserialized JSON object.
222
223 Returns:
224 An instance of a Credentials subclass.
225 """
226 return Credentials()
227
228
229 -class Flow(object):
230 """Base class for all Flow objects."""
231 pass
232
235 """Base class for all Storage objects.
236
237 Store and retrieve a single credential. This class supports locking
238 such that multiple processes and threads can operate on a single
239 store.
240 """
241
243 """Acquires any lock necessary to access this Storage.
244
245 This lock is not reentrant.
246 """
247 pass
248
250 """Release the Storage lock.
251
252 Trying to release a lock that isn't held will result in a
253 RuntimeError.
254 """
255 pass
256
258 """Retrieve credential.
259
260 The Storage lock must be held when this is called.
261
262 Returns:
263 oauth2client.client.Credentials
264 """
265 _abstract()
266
268 """Write a credential.
269
270 The Storage lock must be held when this is called.
271
272 Args:
273 credentials: Credentials, the credentials to store.
274 """
275 _abstract()
276
278 """Delete a credential.
279
280 The Storage lock must be held when this is called.
281 """
282 _abstract()
283
285 """Retrieve credential.
286
287 The Storage lock must *not* be held when this is called.
288
289 Returns:
290 oauth2client.client.Credentials
291 """
292 self.acquire_lock()
293 try:
294 return self.locked_get()
295 finally:
296 self.release_lock()
297
298 - def put(self, credentials):
299 """Write a credential.
300
301 The Storage lock must be held when this is called.
302
303 Args:
304 credentials: Credentials, the credentials to store.
305 """
306 self.acquire_lock()
307 try:
308 self.locked_put(credentials)
309 finally:
310 self.release_lock()
311
313 """Delete credential.
314
315 Frees any resources associated with storing the credential.
316 The Storage lock must *not* be held when this is called.
317
318 Returns:
319 None
320 """
321 self.acquire_lock()
322 try:
323 return self.locked_delete()
324 finally:
325 self.release_lock()
326
329 """Forces header keys and values to be strings, i.e not unicode.
330
331 The httplib module just concats the header keys and values in a way that may
332 make the message header a unicode string, which, if it then tries to
333 contatenate to a binary request body may result in a unicode decode error.
334
335 Args:
336 headers: dict, A dictionary of headers.
337
338 Returns:
339 The same dictionary but with all the keys converted to strings.
340 """
341 clean = {}
342 try:
343 for k, v in headers.iteritems():
344 clean[str(k)] = str(v)
345 except UnicodeEncodeError:
346 raise NonAsciiHeaderError(k + ': ' + v)
347 return clean
348
351 """Credentials object for OAuth 2.0.
352
353 Credentials can be applied to an httplib2.Http object using the authorize()
354 method, which then adds the OAuth 2.0 access token to each request.
355
356 OAuth2Credentials objects may be safely pickled and unpickled.
357 """
358
359 @util.positional(8)
360 - def __init__(self, access_token, client_id, client_secret, refresh_token,
361 token_expiry, token_uri, user_agent, id_token=None):
362 """Create an instance of OAuth2Credentials.
363
364 This constructor is not usually called by the user, instead
365 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
366
367 Args:
368 access_token: string, access token.
369 client_id: string, client identifier.
370 client_secret: string, client secret.
371 refresh_token: string, refresh token.
372 token_expiry: datetime, when the access_token expires.
373 token_uri: string, URI of token endpoint.
374 user_agent: string, The HTTP User-Agent to provide for this application.
375 id_token: object, The identity of the resource owner.
376
377 Notes:
378 store: callable, A callable that when passed a Credential
379 will store the credential back to where it came from.
380 This is needed to store the latest access_token if it
381 has expired and been refreshed.
382 """
383 self.access_token = access_token
384 self.client_id = client_id
385 self.client_secret = client_secret
386 self.refresh_token = refresh_token
387 self.store = None
388 self.token_expiry = token_expiry
389 self.token_uri = token_uri
390 self.user_agent = user_agent
391 self.id_token = id_token
392
393
394
395 self.invalid = False
396
398 """Authorize an httplib2.Http instance with these credentials.
399
400 The modified http.request method will add authentication headers to each
401 request and will refresh access_tokens when a 401 is received on a
402 request. In addition the http.request method has a credentials property,
403 http.request.credentials, which is the Credentials object that authorized
404 it.
405
406 Args:
407 http: An instance of httplib2.Http
408 or something that acts like it.
409
410 Returns:
411 A modified instance of http that was passed in.
412
413 Example:
414
415 h = httplib2.Http()
416 h = credentials.authorize(h)
417
418 You can't create a new OAuth subclass of httplib2.Authenication
419 because it never gets passed the absolute URI, which is needed for
420 signing. So instead we have to overload 'request' with a closure
421 that adds in the Authorization header and then calls the original
422 version of 'request()'.
423 """
424 request_orig = http.request
425
426
427 @util.positional(1)
428 def new_request(uri, method='GET', body=None, headers=None,
429 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
430 connection_type=None):
431 if not self.access_token:
432 logger.info('Attempting refresh to obtain initial access_token')
433 self._refresh(request_orig)
434
435
436
437 if headers is None:
438 headers = {}
439 self.apply(headers)
440
441 if self.user_agent is not None:
442 if 'user-agent' in headers:
443 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
444 else:
445 headers['user-agent'] = self.user_agent
446
447 resp, content = request_orig(uri, method, body, clean_headers(headers),
448 redirections, connection_type)
449
450 if resp.status in REFRESH_STATUS_CODES:
451 logger.info('Refreshing due to a %s' % str(resp.status))
452 self._refresh(request_orig)
453 self.apply(headers)
454 return request_orig(uri, method, body, clean_headers(headers),
455 redirections, connection_type)
456 else:
457 return (resp, content)
458
459
460 http.request = new_request
461
462
463 setattr(http.request, 'credentials', self)
464
465 return http
466
468 """Forces a refresh of the access_token.
469
470 Args:
471 http: httplib2.Http, an http object to be used to make the refresh
472 request.
473 """
474 self._refresh(http.request)
475
476 - def apply(self, headers):
477 """Add the authorization to the headers.
478
479 Args:
480 headers: dict, the headers to add the Authorization header to.
481 """
482 headers['Authorization'] = 'Bearer ' + self.access_token
483
486
487 @classmethod
489 """Instantiate a Credentials object from a JSON description of it. The JSON
490 should have been produced by calling .to_json() on the object.
491
492 Args:
493 data: dict, A deserialized JSON object.
494
495 Returns:
496 An instance of a Credentials subclass.
497 """
498 data = simplejson.loads(s)
499 if 'token_expiry' in data and not isinstance(data['token_expiry'],
500 datetime.datetime):
501 try:
502 data['token_expiry'] = datetime.datetime.strptime(
503 data['token_expiry'], EXPIRY_FORMAT)
504 except:
505 data['token_expiry'] = None
506 retval = OAuth2Credentials(
507 data['access_token'],
508 data['client_id'],
509 data['client_secret'],
510 data['refresh_token'],
511 data['token_expiry'],
512 data['token_uri'],
513 data['user_agent'],
514 id_token=data.get('id_token', None))
515 retval.invalid = data['invalid']
516 return retval
517
518 @property
520 """True if the credential is expired or invalid.
521
522 If the token_expiry isn't set, we assume the token doesn't expire.
523 """
524 if self.invalid:
525 return True
526
527 if not self.token_expiry:
528 return False
529
530 now = datetime.datetime.utcnow()
531 if now >= self.token_expiry:
532 logger.info('access_token is expired. Now: %s, token_expiry: %s',
533 now, self.token_expiry)
534 return True
535 return False
536
538 """Set the Storage for the credential.
539
540 Args:
541 store: Storage, an implementation of Stroage object.
542 This is needed to store the latest access_token if it
543 has expired and been refreshed. This implementation uses
544 locking to check for updates before updating the
545 access_token.
546 """
547 self.store = store
548
550 """Update this Credential from another instance."""
551 self.__dict__.update(other.__getstate__())
552
554 """Trim the state down to something that can be pickled."""
555 d = copy.copy(self.__dict__)
556 del d['store']
557 return d
558
560 """Reconstitute the state of the object from being pickled."""
561 self.__dict__.update(state)
562 self.store = None
563
565 """Generate the body that will be used in the refresh request."""
566 body = urllib.urlencode({
567 'grant_type': 'refresh_token',
568 'client_id': self.client_id,
569 'client_secret': self.client_secret,
570 'refresh_token': self.refresh_token,
571 })
572 return body
573
575 """Generate the headers that will be used in the refresh request."""
576 headers = {
577 'content-type': 'application/x-www-form-urlencoded',
578 }
579
580 if self.user_agent is not None:
581 headers['user-agent'] = self.user_agent
582
583 return headers
584
586 """Refreshes the access_token.
587
588 This method first checks by reading the Storage object if available.
589 If a refresh is still needed, it holds the Storage lock until the
590 refresh is completed.
591
592 Args:
593 http_request: callable, a callable that matches the method signature of
594 httplib2.Http.request, used to make the refresh request.
595
596 Raises:
597 AccessTokenRefreshError: When the refresh fails.
598 """
599 if not self.store:
600 self._do_refresh_request(http_request)
601 else:
602 self.store.acquire_lock()
603 try:
604 new_cred = self.store.locked_get()
605 if (new_cred and not new_cred.invalid and
606 new_cred.access_token != self.access_token):
607 logger.info('Updated access_token read from Storage')
608 self._updateFromCredential(new_cred)
609 else:
610 self._do_refresh_request(http_request)
611 finally:
612 self.store.release_lock()
613
615 """Refresh the access_token using the refresh_token.
616
617 Args:
618 http_request: callable, a callable that matches the method signature of
619 httplib2.Http.request, used to make the refresh request.
620
621 Raises:
622 AccessTokenRefreshError: When the refresh fails.
623 """
624 body = self._generate_refresh_request_body()
625 headers = self._generate_refresh_request_headers()
626
627 logger.info('Refreshing access_token')
628 resp, content = http_request(
629 self.token_uri, method='POST', body=body, headers=headers)
630 if resp.status == 200:
631
632 d = simplejson.loads(content)
633 self.access_token = d['access_token']
634 self.refresh_token = d.get('refresh_token', self.refresh_token)
635 if 'expires_in' in d:
636 self.token_expiry = datetime.timedelta(
637 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
638 else:
639 self.token_expiry = None
640 if self.store:
641 self.store.locked_put(self)
642 else:
643
644
645 logger.info('Failed to retrieve access token: %s' % content)
646 error_msg = 'Invalid response %s.' % resp['status']
647 try:
648 d = simplejson.loads(content)
649 if 'error' in d:
650 error_msg = d['error']
651 self.invalid = True
652 if self.store:
653 self.store.locked_put(self)
654 except StandardError:
655 pass
656 raise AccessTokenRefreshError(error_msg)
657
660 """Credentials object for OAuth 2.0.
661
662 Credentials can be applied to an httplib2.Http object using the
663 authorize() method, which then signs each request from that object
664 with the OAuth 2.0 access token. This set of credentials is for the
665 use case where you have acquired an OAuth 2.0 access_token from
666 another place such as a JavaScript client or another web
667 application, and wish to use it from Python. Because only the
668 access_token is present it can not be refreshed and will in time
669 expire.
670
671 AccessTokenCredentials objects may be safely pickled and unpickled.
672
673 Usage:
674 credentials = AccessTokenCredentials('<an access token>',
675 'my-user-agent/1.0')
676 http = httplib2.Http()
677 http = credentials.authorize(http)
678
679 Exceptions:
680 AccessTokenCredentialsExpired: raised when the access_token expires or is
681 revoked.
682 """
683
684 - def __init__(self, access_token, user_agent):
685 """Create an instance of OAuth2Credentials
686
687 This is one of the few types if Credentials that you should contrust,
688 Credentials objects are usually instantiated by a Flow.
689
690 Args:
691 access_token: string, access token.
692 user_agent: string, The HTTP User-Agent to provide for this application.
693
694 Notes:
695 store: callable, a callable that when passed a Credential
696 will store the credential back to where it came from.
697 """
698 super(AccessTokenCredentials, self).__init__(
699 access_token,
700 None,
701 None,
702 None,
703 None,
704 None,
705 user_agent)
706
707
708 @classmethod
710 data = simplejson.loads(s)
711 retval = AccessTokenCredentials(
712 data['access_token'],
713 data['user_agent'])
714 return retval
715
719
722 """Abstract Credentials object used for OAuth 2.0 assertion grants.
723
724 This credential does not require a flow to instantiate because it
725 represents a two legged flow, and therefore has all of the required
726 information to generate and refresh its own access tokens. It must
727 be subclassed to generate the appropriate assertion string.
728
729 AssertionCredentials objects may be safely pickled and unpickled.
730 """
731
732 @util.positional(2)
733 - def __init__(self, assertion_type, user_agent=None,
734 token_uri='https://accounts.google.com/o/oauth2/token',
735 **unused_kwargs):
736 """Constructor for AssertionFlowCredentials.
737
738 Args:
739 assertion_type: string, assertion type that will be declared to the auth
740 server
741 user_agent: string, The HTTP User-Agent to provide for this application.
742 token_uri: string, URI for token endpoint. For convenience
743 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
744 """
745 super(AssertionCredentials, self).__init__(
746 None,
747 None,
748 None,
749 None,
750 None,
751 token_uri,
752 user_agent)
753 self.assertion_type = assertion_type
754
756 assertion = self._generate_assertion()
757
758 body = urllib.urlencode({
759 'assertion_type': self.assertion_type,
760 'assertion': assertion,
761 'grant_type': 'assertion',
762 })
763
764 return body
765
767 """Generate the assertion string that will be used in the access token
768 request.
769 """
770 _abstract()
771
772 if HAS_CRYPTO:
778 """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
779
780 This credential does not require a flow to instantiate because it represents
781 a two legged flow, and therefore has all of the required information to
782 generate and refresh its own access tokens.
783
784 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or
785 later. For App Engine you may also consider using AppAssertionCredentials.
786 """
787
788 MAX_TOKEN_LIFETIME_SECS = 3600
789
790 @util.positional(4)
791 - def __init__(self,
792 service_account_name,
793 private_key,
794 scope,
795 private_key_password='notasecret',
796 user_agent=None,
797 token_uri='https://accounts.google.com/o/oauth2/token',
798 **kwargs):
799 """Constructor for SignedJwtAssertionCredentials.
800
801 Args:
802 service_account_name: string, id for account, usually an email address.
803 private_key: string, private key in PKCS12 or PEM format.
804 scope: string or iterable of strings, scope(s) of the credentials being
805 requested.
806 private_key_password: string, password for private_key, unused if
807 private_key is in PEM format.
808 user_agent: string, HTTP User-Agent to provide for this application.
809 token_uri: string, URI for token endpoint. For convenience
810 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
811 kwargs: kwargs, Additional parameters to add to the JWT token, for
812 example prn=joe@xample.org."""
813
814 super(SignedJwtAssertionCredentials, self).__init__(
815 'http://oauth.net/grant_type/jwt/1.0/bearer',
816 user_agent=user_agent,
817 token_uri=token_uri,
818 )
819
820 self.scope = util.scopes_to_string(scope)
821
822
823 self.private_key = base64.b64encode(private_key)
824
825 self.private_key_password = private_key_password
826 self.service_account_name = service_account_name
827 self.kwargs = kwargs
828
829 @classmethod
831 data = simplejson.loads(s)
832 retval = SignedJwtAssertionCredentials(
833 data['service_account_name'],
834 base64.b64decode(data['private_key']),
835 data['scope'],
836 private_key_password=data['private_key_password'],
837 user_agent=data['user_agent'],
838 token_uri=data['token_uri'],
839 **data['kwargs']
840 )
841 retval.invalid = data['invalid']
842 retval.access_token = data['access_token']
843 return retval
844
846 """Generate the assertion that will be used in the request."""
847 now = long(time.time())
848 payload = {
849 'aud': self.token_uri,
850 'scope': self.scope,
851 'iat': now,
852 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
853 'iss': self.service_account_name
854 }
855 payload.update(self.kwargs)
856 logger.debug(str(payload))
857
858 private_key = base64.b64decode(self.private_key)
859 return crypt.make_signed_jwt(crypt.Signer.from_string(
860 private_key, self.private_key_password), payload)
861
862
863
864 _cached_http = httplib2.Http(MemoryCache())
865
866 @util.positional(2)
869 """Verifies a signed JWT id_token.
870
871 This function requires PyOpenSSL and because of that it does not work on
872 App Engine.
873
874 Args:
875 id_token: string, A Signed JWT.
876 audience: string, The audience 'aud' that the token should be for.
877 http: httplib2.Http, instance to use to make the HTTP request. Callers
878 should supply an instance that has caching enabled.
879 cert_uri: string, URI of the certificates in JSON format to
880 verify the JWT against.
881
882 Returns:
883 The deserialized JSON in the JWT.
884
885 Raises:
886 oauth2client.crypt.AppIdentityError if the JWT fails to verify.
887 """
888 if http is None:
889 http = _cached_http
890
891 resp, content = http.request(cert_uri)
892
893 if resp.status == 200:
894 certs = simplejson.loads(content)
895 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
896 else:
897 raise VerifyJwtTokenError('Status code: %d' % resp.status)
898
901
902 b64string = b64string.encode('ascii')
903 padded = b64string + '=' * (4 - len(b64string) % 4)
904 return base64.urlsafe_b64decode(padded)
905
908 """Extract the JSON payload from a JWT.
909
910 Does the extraction w/o checking the signature.
911
912 Args:
913 id_token: string, OAuth 2.0 id_token.
914
915 Returns:
916 object, The deserialized JSON payload.
917 """
918 segments = id_token.split('.')
919
920 if (len(segments) != 3):
921 raise VerifyJwtTokenError(
922 'Wrong number of segments in token: %s' % id_token)
923
924 return simplejson.loads(_urlsafe_b64decode(segments[1]))
925
928 """Parses response of an exchange token request.
929
930 Most providers return JSON but some (e.g. Facebook) return a
931 url-encoded string.
932
933 Args:
934 content: The body of a response
935
936 Returns:
937 Content as a dictionary object. Note that the dict could be empty,
938 i.e. {}. That basically indicates a failure.
939 """
940 resp = {}
941 try:
942 resp = simplejson.loads(content)
943 except StandardError:
944
945
946 resp = dict(parse_qsl(content))
947
948
949 if resp and 'expires' in resp:
950 resp['expires_in'] = resp.pop('expires')
951
952 return resp
953
954
955 @util.positional(4)
956 -def credentials_from_code(client_id, client_secret, scope, code,
957 redirect_uri='postmessage', http=None, user_agent=None,
958 token_uri='https://accounts.google.com/o/oauth2/token'):
959 """Exchanges an authorization code for an OAuth2Credentials object.
960
961 Args:
962 client_id: string, client identifier.
963 client_secret: string, client secret.
964 scope: string or iterable of strings, scope(s) to request.
965 code: string, An authroization code, most likely passed down from
966 the client
967 redirect_uri: string, this is generally set to 'postmessage' to match the
968 redirect_uri that the client specified
969 http: httplib2.Http, optional http instance to use to do the fetch
970 token_uri: string, URI for token endpoint. For convenience
971 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
972 Returns:
973 An OAuth2Credentials object.
974
975 Raises:
976 FlowExchangeError if the authorization code cannot be exchanged for an
977 access token
978 """
979 flow = OAuth2WebServerFlow(client_id, client_secret, scope,
980 redirect_uri=redirect_uri, user_agent=user_agent,
981 auth_uri='https://accounts.google.com/o/oauth2/auth',
982 token_uri=token_uri)
983
984 credentials = flow.step2_exchange(code, http=http)
985 return credentials
986
994 """Returns OAuth2Credentials from a clientsecrets file and an auth code.
995
996 Will create the right kind of Flow based on the contents of the clientsecrets
997 file or will raise InvalidClientSecretsError for unknown types of Flows.
998
999 Args:
1000 filename: string, File name of clientsecrets.
1001 scope: string or iterable of strings, scope(s) to request.
1002 code: string, An authorization code, most likely passed down from
1003 the client
1004 message: string, A friendly string to display to the user if the
1005 clientsecrets file is missing or invalid. If message is provided then
1006 sys.exit will be called in the case of an error. If message in not
1007 provided then clientsecrets.InvalidClientSecretsError will be raised.
1008 redirect_uri: string, this is generally set to 'postmessage' to match the
1009 redirect_uri that the client specified
1010 http: httplib2.Http, optional http instance to use to do the fetch
1011 cache: An optional cache service client that implements get() and set()
1012 methods. See clientsecrets.loadfile() for details.
1013
1014 Returns:
1015 An OAuth2Credentials object.
1016
1017 Raises:
1018 FlowExchangeError if the authorization code cannot be exchanged for an
1019 access token
1020 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1021 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1022 invalid.
1023 """
1024 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
1025 redirect_uri=redirect_uri)
1026 credentials = flow.step2_exchange(code, http=http)
1027 return credentials
1028
1031 """Does the Web Server Flow for OAuth 2.0.
1032
1033 OAuth2WebServerFlow objects may be safely pickled and unpickled.
1034 """
1035
1036 @util.positional(4)
1037 - def __init__(self, client_id, client_secret, scope,
1038 redirect_uri=None,
1039 user_agent=None,
1040 auth_uri='https://accounts.google.com/o/oauth2/auth',
1041 token_uri='https://accounts.google.com/o/oauth2/token',
1042 **kwargs):
1043 """Constructor for OAuth2WebServerFlow.
1044
1045 The kwargs argument is used to set extra query parameters on the
1046 auth_uri. For example, the access_type and approval_prompt
1047 query parameters can be set via kwargs.
1048
1049 Args:
1050 client_id: string, client identifier.
1051 client_secret: string client secret.
1052 scope: string or iterable of strings, scope(s) of the credentials being
1053 requested.
1054 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1055 a non-web-based application, or a URI that handles the callback from
1056 the authorization server.
1057 user_agent: string, HTTP User-Agent to provide for this application.
1058 auth_uri: string, URI for authorization endpoint. For convenience
1059 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1060 token_uri: string, URI for token endpoint. For convenience
1061 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1062 **kwargs: dict, The keyword arguments are all optional and required
1063 parameters for the OAuth calls.
1064 """
1065 self.client_id = client_id
1066 self.client_secret = client_secret
1067 self.scope = util.scopes_to_string(scope)
1068 self.redirect_uri = redirect_uri
1069 self.user_agent = user_agent
1070 self.auth_uri = auth_uri
1071 self.token_uri = token_uri
1072 self.params = {
1073 'access_type': 'offline',
1074 'response_type': 'code',
1075 }
1076 self.params.update(kwargs)
1077
1078 @util.positional(1)
1080 """Returns a URI to redirect to the provider.
1081
1082 Args:
1083 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1084 a non-web-based application, or a URI that handles the callback from
1085 the authorization server. This parameter is deprecated, please move to
1086 passing the redirect_uri in via the constructor.
1087
1088 Returns:
1089 A URI as a string to redirect the user to begin the authorization flow.
1090 """
1091 if redirect_uri is not None:
1092 logger.warning(('The redirect_uri parameter for'
1093 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
1094 'move to passing the redirect_uri in via the constructor.'))
1095 self.redirect_uri = redirect_uri
1096
1097 if self.redirect_uri is None:
1098 raise ValueError('The value of redirect_uri must not be None.')
1099
1100 query = {
1101 'client_id': self.client_id,
1102 'redirect_uri': self.redirect_uri,
1103 'scope': self.scope,
1104 }
1105 query.update(self.params)
1106 parts = list(urlparse.urlparse(self.auth_uri))
1107 query.update(dict(parse_qsl(parts[4])))
1108 parts[4] = urllib.urlencode(query)
1109 return urlparse.urlunparse(parts)
1110
1111 @util.positional(2)
1113 """Exhanges a code for OAuth2Credentials.
1114
1115 Args:
1116 code: string or dict, either the code as a string, or a dictionary
1117 of the query parameters to the redirect_uri, which contains
1118 the code.
1119 http: httplib2.Http, optional http instance to use to do the fetch
1120
1121 Returns:
1122 An OAuth2Credentials object that can be used to authorize requests.
1123
1124 Raises:
1125 FlowExchangeError if a problem occured exchanging the code for a
1126 refresh_token.
1127 """
1128
1129 if not (isinstance(code, str) or isinstance(code, unicode)):
1130 if 'code' not in code:
1131 if 'error' in code:
1132 error_msg = code['error']
1133 else:
1134 error_msg = 'No code was supplied in the query parameters.'
1135 raise FlowExchangeError(error_msg)
1136 else:
1137 code = code['code']
1138
1139 body = urllib.urlencode({
1140 'grant_type': 'authorization_code',
1141 'client_id': self.client_id,
1142 'client_secret': self.client_secret,
1143 'code': code,
1144 'redirect_uri': self.redirect_uri,
1145 'scope': self.scope,
1146 })
1147 headers = {
1148 'content-type': 'application/x-www-form-urlencoded',
1149 }
1150
1151 if self.user_agent is not None:
1152 headers['user-agent'] = self.user_agent
1153
1154 if http is None:
1155 http = httplib2.Http()
1156
1157 resp, content = http.request(self.token_uri, method='POST', body=body,
1158 headers=headers)
1159 d = _parse_exchange_token_response(content)
1160 if resp.status == 200 and 'access_token' in d:
1161 access_token = d['access_token']
1162 refresh_token = d.get('refresh_token', None)
1163 token_expiry = None
1164 if 'expires_in' in d:
1165 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
1166 seconds=int(d['expires_in']))
1167
1168 if 'id_token' in d:
1169 d['id_token'] = _extract_id_token(d['id_token'])
1170
1171 logger.info('Successfully retrieved access token')
1172 return OAuth2Credentials(access_token, self.client_id,
1173 self.client_secret, refresh_token, token_expiry,
1174 self.token_uri, self.user_agent,
1175 id_token=d.get('id_token', None))
1176 else:
1177 logger.info('Failed to retrieve access token: %s' % content)
1178 if 'error' in d:
1179
1180 error_msg = unicode(d['error'])
1181 else:
1182 error_msg = 'Invalid response: %s.' % str(resp.status)
1183 raise FlowExchangeError(error_msg)
1184
1188 """Create a Flow from a clientsecrets file.
1189
1190 Will create the right kind of Flow based on the contents of the clientsecrets
1191 file or will raise InvalidClientSecretsError for unknown types of Flows.
1192
1193 Args:
1194 filename: string, File name of client secrets.
1195 scope: string or iterable of strings, scope(s) to request.
1196 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
1197 a non-web-based application, or a URI that handles the callback from
1198 the authorization server.
1199 message: string, A friendly string to display to the user if the
1200 clientsecrets file is missing or invalid. If message is provided then
1201 sys.exit will be called in the case of an error. If message in not
1202 provided then clientsecrets.InvalidClientSecretsError will be raised.
1203 cache: An optional cache service client that implements get() and set()
1204 methods. See clientsecrets.loadfile() for details.
1205
1206 Returns:
1207 A Flow object.
1208
1209 Raises:
1210 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
1211 clientsecrets.InvalidClientSecretsError if the clientsecrets file is
1212 invalid.
1213 """
1214 try:
1215 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
1216 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
1217 return OAuth2WebServerFlow(
1218 client_info['client_id'],
1219 client_info['client_secret'],
1220 scope,
1221 redirect_uri=redirect_uri,
1222 user_agent=None,
1223 auth_uri=client_info['auth_uri'],
1224 token_uri=client_info['token_uri'])
1225
1226 except clientsecrets.InvalidClientSecretsError:
1227 if message:
1228 sys.exit(message)
1229 else:
1230 raise
1231 else:
1232 raise UnknownClientSecretsFlowError(
1233 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
1234