/gdata/gauth.py
Python | 1306 lines | 1241 code | 18 blank | 47 comment | 25 complexity | 2ad17ed286a07f9fc81ff8a3fad7f979 MD5 | raw file
1#!/usr/bin/env python 2# 3# Copyright (C) 2009 Google Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17 18# This module is used for version 2 of the Google Data APIs. 19 20 21"""Provides auth related token classes and functions for Google Data APIs. 22 23Token classes represent a user's authorization of this app to access their 24data. Usually these are not created directly but by a GDClient object. 25 26ClientLoginToken 27AuthSubToken 28SecureAuthSubToken 29OAuthHmacToken 30OAuthRsaToken 31TwoLeggedOAuthHmacToken 32TwoLeggedOAuthRsaToken 33 34Functions which are often used in application code (as opposed to just within 35the gdata-python-client library) are the following: 36 37generate_auth_sub_url 38authorize_request_token 39 40The following are helper functions which are used to save and load auth token 41objects in the App Engine datastore. These should only be used if you are using 42this library within App Engine: 43 44ae_load 45ae_save 46""" 47 48 49import time 50import random 51import urllib 52import atom.http_core 53 54 55__author__ = 'j.s@google.com (Jeff Scudder)' 56 57 58PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth=' 59AUTHSUB_AUTH_LABEL = 'AuthSub token=' 60 61 62# This dict provides the AuthSub and OAuth scopes for all services by service 63# name. The service name (key) is used in ClientLogin requests. 64AUTH_SCOPES = { 65 'cl': ( # Google Calendar API 66 'https://www.google.com/calendar/feeds/', 67 'http://www.google.com/calendar/feeds/'), 68 'gbase': ( # Google Base API 69 'http://base.google.com/base/feeds/', 70 'http://www.google.com/base/feeds/'), 71 'blogger': ( # Blogger API 72 'http://www.blogger.com/feeds/',), 73 'codesearch': ( # Google Code Search API 74 'http://www.google.com/codesearch/feeds/',), 75 'cp': ( # Contacts API 76 'https://www.google.com/m8/feeds/', 77 'http://www.google.com/m8/feeds/'), 78 'finance': ( # Google Finance API 79 'http://finance.google.com/finance/feeds/',), 80 'health': ( # Google Health API 81 'https://www.google.com/health/feeds/',), 82 'writely': ( # Documents List API 83 'https://docs.google.com/feeds/', 84 'http://docs.google.com/feeds/'), 85 'lh2': ( # Picasa Web Albums API 86 'http://picasaweb.google.com/data/',), 87 'apps': ( # Google Apps Provisioning API 88 'http://www.google.com/a/feeds/', 89 'https://www.google.com/a/feeds/', 90 'http://apps-apis.google.com/a/feeds/', 91 'https://apps-apis.google.com/a/feeds/'), 92 'weaver': ( # Health H9 Sandbox 93 'https://www.google.com/h9/feeds/',), 94 'wise': ( # Spreadsheets Data API 95 'https://spreadsheets.google.com/feeds/', 96 'http://spreadsheets.google.com/feeds/'), 97 'sitemaps': ( # Google Webmaster Tools API 98 'https://www.google.com/webmasters/tools/feeds/',), 99 'youtube': ( # YouTube API 100 'http://gdata.youtube.com/feeds/api/', 101 'http://uploads.gdata.youtube.com/feeds/api', 102 'http://gdata.youtube.com/action/GetUploadToken'), 103 'books': ( # Google Books API 104 'http://www.google.com/books/feeds/',), 105 'analytics': ( # Google Analytics API 106 'https://www.google.com/analytics/feeds/',), 107 'jotspot': ( # Google Sites API 108 'http://sites.google.com/feeds/', 109 'https://sites.google.com/feeds/'), 110 'local': ( # Google Maps Data API 111 'http://maps.google.com/maps/feeds/',), 112 'code': ( # Project Hosting Data API 113 'http://code.google.com/feeds/issues',)} 114 115 116 117class Error(Exception): 118 pass 119 120 121class UnsupportedTokenType(Error): 122 """Raised when token to or from blob is unable to convert the token.""" 123 pass 124 125 126# ClientLogin functions and classes. 127def generate_client_login_request_body(email, password, service, source, 128 account_type='HOSTED_OR_GOOGLE', captcha_token=None, 129 captcha_response=None): 130 """Creates the body of the autentication request 131 132 See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request 133 for more details. 134 135 Args: 136 email: str 137 password: str 138 service: str 139 source: str 140 account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid 141 values are 'GOOGLE' and 'HOSTED' 142 captcha_token: str (optional) 143 captcha_response: str (optional) 144 145 Returns: 146 The HTTP body to send in a request for a client login token. 147 """ 148 # Create a POST body containing the user's credentials. 149 request_fields = {'Email': email, 150 'Passwd': password, 151 'accountType': account_type, 152 'service': service, 153 'source': source} 154 if captcha_token and captcha_response: 155 # Send the captcha token and response as part of the POST body if the 156 # user is responding to a captch challenge. 157 request_fields['logintoken'] = captcha_token 158 request_fields['logincaptcha'] = captcha_response 159 return urllib.urlencode(request_fields) 160 161 162GenerateClientLoginRequestBody = generate_client_login_request_body 163 164 165def get_client_login_token_string(http_body): 166 """Returns the token value for a ClientLoginToken. 167 168 Reads the token from the server's response to a Client Login request and 169 creates the token value string to use in requests. 170 171 Args: 172 http_body: str The body of the server's HTTP response to a Client Login 173 request 174 175 Returns: 176 The token value string for a ClientLoginToken. 177 """ 178 for response_line in http_body.splitlines(): 179 if response_line.startswith('Auth='): 180 # Strip off the leading Auth= and return the Authorization value. 181 return response_line[5:] 182 return None 183 184 185GetClientLoginTokenString = get_client_login_token_string 186 187 188def get_captcha_challenge(http_body, 189 captcha_base_url='http://www.google.com/accounts/'): 190 """Returns the URL and token for a CAPTCHA challenge issued by the server. 191 192 Args: 193 http_body: str The body of the HTTP response from the server which 194 contains the CAPTCHA challenge. 195 captcha_base_url: str This function returns a full URL for viewing the 196 challenge image which is built from the server's response. This 197 base_url is used as the beginning of the URL because the server 198 only provides the end of the URL. For example the server provides 199 'Captcha?ctoken=Hi...N' and the URL for the image is 200 'http://www.google.com/accounts/Captcha?ctoken=Hi...N' 201 202 Returns: 203 A dictionary containing the information needed to repond to the CAPTCHA 204 challenge, the image URL and the ID token of the challenge. The 205 dictionary is in the form: 206 {'token': string identifying the CAPTCHA image, 207 'url': string containing the URL of the image} 208 Returns None if there was no CAPTCHA challenge in the response. 209 """ 210 contains_captcha_challenge = False 211 captcha_parameters = {} 212 for response_line in http_body.splitlines(): 213 if response_line.startswith('Error=CaptchaRequired'): 214 contains_captcha_challenge = True 215 elif response_line.startswith('CaptchaToken='): 216 # Strip off the leading CaptchaToken= 217 captcha_parameters['token'] = response_line[13:] 218 elif response_line.startswith('CaptchaUrl='): 219 captcha_parameters['url'] = '%s%s' % (captcha_base_url, 220 response_line[11:]) 221 if contains_captcha_challenge: 222 return captcha_parameters 223 else: 224 return None 225 226 227GetCaptchaChallenge = get_captcha_challenge 228 229 230class ClientLoginToken(object): 231 232 def __init__(self, token_string): 233 self.token_string = token_string 234 235 def modify_request(self, http_request): 236 http_request.headers['Authorization'] = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, 237 self.token_string) 238 239 ModifyRequest = modify_request 240 241 242# AuthSub functions and classes. 243def _to_uri(str_or_uri): 244 if isinstance(str_or_uri, (str, unicode)): 245 return atom.http_core.Uri.parse_uri(str_or_uri) 246 return str_or_uri 247 248 249def generate_auth_sub_url(next, scopes, secure=False, session=True, 250 request_url=atom.http_core.parse_uri( 251 'https://www.google.com/accounts/AuthSubRequest'), 252 domain='default', scopes_param_prefix='auth_sub_scopes'): 253 """Constructs a URI for requesting a multiscope AuthSub token. 254 255 The generated token will contain a URL parameter to pass along the 256 requested scopes to the next URL. When the Google Accounts page 257 redirects the broswser to the 'next' URL, it appends the single use 258 AuthSub token value to the URL as a URL parameter with the key 'token'. 259 However, the information about which scopes were requested is not 260 included by Google Accounts. This method adds the scopes to the next 261 URL before making the request so that the redirect will be sent to 262 a page, and both the token value and the list of scopes for which the token 263 was requested. 264 265 Args: 266 next: atom.http_core.Uri or string The URL user will be sent to after 267 authorizing this web application to access their data. 268 scopes: list containint strings or atom.http_core.Uri objects. The URLs 269 of the services to be accessed. Could also be a single string 270 or single atom.http_core.Uri for requesting just one scope. 271 secure: boolean (optional) Determines whether or not the issued token 272 is a secure token. 273 session: boolean (optional) Determines whether or not the issued token 274 can be upgraded to a session token. 275 request_url: atom.http_core.Uri or str The beginning of the request URL. 276 This is normally 277 'http://www.google.com/accounts/AuthSubRequest' or 278 '/accounts/AuthSubRequest' 279 domain: The domain which the account is part of. This is used for Google 280 Apps accounts, the default value is 'default' which means that 281 the requested account is a Google Account (@gmail.com for 282 example) 283 scopes_param_prefix: str (optional) The requested scopes are added as a 284 URL parameter to the next URL so that the page at 285 the 'next' URL can extract the token value and the 286 valid scopes from the URL. The key for the URL 287 parameter defaults to 'auth_sub_scopes' 288 289 Returns: 290 An atom.http_core.Uri which the user's browser should be directed to in 291 order to authorize this application to access their information. 292 """ 293 if isinstance(next, (str, unicode)): 294 next = atom.http_core.Uri.parse_uri(next) 295 # If the user passed in a string instead of a list for scopes, convert to 296 # a single item tuple. 297 if isinstance(scopes, (str, unicode, atom.http_core.Uri)): 298 scopes = (scopes,) 299 scopes_string = ' '.join([str(scope) for scope in scopes]) 300 next.query[scopes_param_prefix] = scopes_string 301 302 if isinstance(request_url, (str, unicode)): 303 request_url = atom.http_core.Uri.parse_uri(request_url) 304 request_url.query['next'] = str(next) 305 request_url.query['scope'] = scopes_string 306 if session: 307 request_url.query['session'] = '1' 308 else: 309 request_url.query['session'] = '0' 310 if secure: 311 request_url.query['secure'] = '1' 312 else: 313 request_url.query['secure'] = '0' 314 request_url.query['hd'] = domain 315 return request_url 316 317 318def auth_sub_string_from_url(url, scopes_param_prefix='auth_sub_scopes'): 319 """Finds the token string (and scopes) after the browser is redirected. 320 321 After the Google Accounts AuthSub pages redirect the user's broswer back to 322 the web application (using the 'next' URL from the request) the web app must 323 extract the token from the current page's URL. The token is provided as a 324 URL parameter named 'token' and if generate_auth_sub_url was used to create 325 the request, the token's valid scopes are included in a URL parameter whose 326 name is specified in scopes_param_prefix. 327 328 Args: 329 url: atom.url.Url or str representing the current URL. The token value 330 and valid scopes should be included as URL parameters. 331 scopes_param_prefix: str (optional) The URL parameter key which maps to 332 the list of valid scopes for the token. 333 334 Returns: 335 A tuple containing the token value as a string, and a tuple of scopes 336 (as atom.http_core.Uri objects) which are URL prefixes under which this 337 token grants permission to read and write user data. 338 (token_string, (scope_uri, scope_uri, scope_uri, ...)) 339 If no scopes were included in the URL, the second value in the tuple is 340 None. If there was no token param in the url, the tuple returned is 341 (None, None) 342 """ 343 if isinstance(url, (str, unicode)): 344 url = atom.http_core.Uri.parse_uri(url) 345 if 'token' not in url.query: 346 return (None, None) 347 token = url.query['token'] 348 # TODO: decide whether no scopes should be None or (). 349 scopes = None # Default to None for no scopes. 350 if scopes_param_prefix in url.query: 351 scopes = tuple(url.query[scopes_param_prefix].split(' ')) 352 return (token, scopes) 353 354 355AuthSubStringFromUrl = auth_sub_string_from_url 356 357 358def auth_sub_string_from_body(http_body): 359 """Extracts the AuthSub token from an HTTP body string. 360 361 Used to find the new session token after making a request to upgrade a 362 single use AuthSub token. 363 364 Args: 365 http_body: str The repsonse from the server which contains the AuthSub 366 key. For example, this function would find the new session token 367 from the server's response to an upgrade token request. 368 369 Returns: 370 The raw token value string to use in an AuthSubToken object. 371 """ 372 for response_line in http_body.splitlines(): 373 if response_line.startswith('Token='): 374 # Strip off Token= and return the token value string. 375 return response_line[6:] 376 return None 377 378 379class AuthSubToken(object): 380 381 def __init__(self, token_string, scopes=None): 382 self.token_string = token_string 383 self.scopes = scopes or [] 384 385 def modify_request(self, http_request): 386 """Sets Authorization header, allows app to act on the user's behalf.""" 387 http_request.headers['Authorization'] = '%s%s' % (AUTHSUB_AUTH_LABEL, 388 self.token_string) 389 390 ModifyRequest = modify_request 391 392 def from_url(str_or_uri): 393 """Creates a new AuthSubToken using information in the URL. 394 395 Uses auth_sub_string_from_url. 396 397 Args: 398 str_or_uri: The current page's URL (as a str or atom.http_core.Uri) 399 which should contain a token query parameter since the 400 Google auth server redirected the user's browser to this 401 URL. 402 """ 403 token_and_scopes = auth_sub_string_from_url(str_or_uri) 404 return AuthSubToken(token_and_scopes[0], token_and_scopes[1]) 405 406 from_url = staticmethod(from_url) 407 FromUrl = from_url 408 409 def _upgrade_token(self, http_body): 410 """Replaces the token value with a session token from the auth server. 411 412 Uses the response of a token upgrade request to modify this token. Uses 413 auth_sub_string_from_body. 414 """ 415 self.token_string = auth_sub_string_from_body(http_body) 416 417 418# Functions and classes for Secure-mode AuthSub 419def build_auth_sub_data(http_request, timestamp, nonce): 420 """Creates the data string which must be RSA-signed in secure requests. 421 422 For more details see the documenation on secure AuthSub requests: 423 http://code.google.com/apis/accounts/docs/AuthSub.html#signingrequests 424 425 Args: 426 http_request: The request being made to the server. The Request's URL 427 must be complete before this signature is calculated as any changes 428 to the URL will invalidate the signature. 429 nonce: str Random 64-bit, unsigned number encoded as an ASCII string in 430 decimal format. The nonce/timestamp pair should always be unique to 431 prevent replay attacks. 432 timestamp: Integer representing the time the request is sent. The 433 timestamp should be expressed in number of seconds after January 1, 434 1970 00:00:00 GMT. 435 """ 436 return '%s %s %s %s' % (http_request.method, str(http_request.uri), 437 str(timestamp), nonce) 438 439 440def generate_signature(data, rsa_key): 441 """Signs the data string for a secure AuthSub request.""" 442 import base64 443 try: 444 from tlslite.utils import keyfactory 445 except ImportError: 446 from gdata.tlslite.utils import keyfactory 447 private_key = keyfactory.parsePrivateKey(rsa_key) 448 signed = private_key.hashAndSign(data) 449 # Python2.3 and lower does not have the base64.b64encode function. 450 if hasattr(base64, 'b64encode'): 451 return base64.b64encode(signed) 452 else: 453 return base64.encodestring(signed).replace('\n', '') 454 455 456class SecureAuthSubToken(AuthSubToken): 457 458 def __init__(self, token_string, rsa_private_key, scopes=None): 459 self.token_string = token_string 460 self.scopes = scopes or [] 461 self.rsa_private_key = rsa_private_key 462 463 def from_url(str_or_uri, rsa_private_key): 464 """Creates a new SecureAuthSubToken using information in the URL. 465 466 Uses auth_sub_string_from_url. 467 468 Args: 469 str_or_uri: The current page's URL (as a str or atom.http_core.Uri) 470 which should contain a token query parameter since the Google auth 471 server redirected the user's browser to this URL. 472 rsa_private_key: str the private RSA key cert used to sign all requests 473 made with this token. 474 """ 475 token_and_scopes = auth_sub_string_from_url(str_or_uri) 476 return SecureAuthSubToken(token_and_scopes[0], rsa_private_key, 477 token_and_scopes[1]) 478 479 from_url = staticmethod(from_url) 480 FromUrl = from_url 481 482 def modify_request(self, http_request): 483 """Sets the Authorization header and includes a digital signature. 484 485 Calculates a digital signature using the private RSA key, a timestamp 486 (uses now at the time this method is called) and a random nonce. 487 488 Args: 489 http_request: The atom.http_core.HttpRequest which contains all of the 490 information needed to send a request to the remote server. The 491 URL and the method of the request must be already set and cannot be 492 changed after this token signs the request, or the signature will 493 not be valid. 494 """ 495 timestamp = str(int(time.time())) 496 nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) 497 data = build_auth_sub_data(http_request, timestamp, nonce) 498 signature = generate_signature(data, self.rsa_private_key) 499 http_request.headers['Authorization'] = ( 500 '%s%s sigalg="rsa-sha1" data="%s" sig="%s"' % (AUTHSUB_AUTH_LABEL, 501 self.token_string, data, signature)) 502 503 ModifyRequest = modify_request 504 505 506# OAuth functions and classes. 507RSA_SHA1 = 'RSA-SHA1' 508HMAC_SHA1 = 'HMAC-SHA1' 509 510 511def build_oauth_base_string(http_request, consumer_key, nonce, signaure_type, 512 timestamp, version, next='oob', token=None, 513 verifier=None): 514 """Generates the base string to be signed in the OAuth request. 515 516 Args: 517 http_request: The request being made to the server. The Request's URL 518 must be complete before this signature is calculated as any changes 519 to the URL will invalidate the signature. 520 consumer_key: Domain identifying the third-party web application. This is 521 the domain used when registering the application with Google. It 522 identifies who is making the request on behalf of the user. 523 nonce: Random 64-bit, unsigned number encoded as an ASCII string in decimal 524 format. The nonce/timestamp pair should always be unique to prevent 525 replay attacks. 526 signaure_type: either RSA_SHA1 or HMAC_SHA1 527 timestamp: Integer representing the time the request is sent. The 528 timestamp should be expressed in number of seconds after January 1, 529 1970 00:00:00 GMT. 530 version: The OAuth version used by the requesting web application. This 531 value must be '1.0' or '1.0a'. If not provided, Google assumes version 532 1.0 is in use. 533 next: The URL the user should be redirected to after granting access 534 to a Google service(s). It can include url-encoded query parameters. 535 The default value is 'oob'. (This is the oauth_callback.) 536 token: The string for the OAuth request token or OAuth access token. 537 verifier: str Sent as the oauth_verifier and required when upgrading a 538 request token to an access token. 539 """ 540 # First we must build the canonical base string for the request. 541 params = http_request.uri.query.copy() 542 params['oauth_consumer_key'] = consumer_key 543 params['oauth_nonce'] = nonce 544 params['oauth_signature_method'] = signaure_type 545 params['oauth_timestamp'] = str(timestamp) 546 if next is not None: 547 params['oauth_callback'] = str(next) 548 if token is not None: 549 params['oauth_token'] = token 550 if version is not None: 551 params['oauth_version'] = version 552 if verifier is not None: 553 params['oauth_verifier'] = verifier 554 # We need to get the key value pairs in lexigraphically sorted order. 555 sorted_keys = None 556 try: 557 sorted_keys = sorted(params.keys()) 558 # The sorted function is not available in Python2.3 and lower 559 except NameError: 560 sorted_keys = params.keys() 561 sorted_keys.sort() 562 pairs = [] 563 for key in sorted_keys: 564 pairs.append('%s=%s' % (urllib.quote(key, safe='~'), 565 urllib.quote(params[key], safe='~'))) 566 # We want to escape /'s too, so use safe='~' 567 all_parameters = urllib.quote('&'.join(pairs), safe='~') 568 normailzed_host = http_request.uri.host.lower() 569 normalized_scheme = (http_request.uri.scheme or 'http').lower() 570 non_default_port = None 571 if (http_request.uri.port is not None 572 and ((normalized_scheme == 'https' and http_request.uri.port != 443) 573 or (normalized_scheme == 'http' and http_request.uri.port != 80))): 574 non_default_port = http_request.uri.port 575 path = http_request.uri.path or '/' 576 request_path = None 577 if not path.startswith('/'): 578 path = '/%s' % path 579 if non_default_port is not None: 580 # Set the only safe char in url encoding to ~ since we want to escape / 581 # as well. 582 request_path = urllib.quote('%s://%s:%s%s' % ( 583 normalized_scheme, normailzed_host, non_default_port, path), safe='~') 584 else: 585 # Set the only safe char in url encoding to ~ since we want to escape / 586 # as well. 587 request_path = urllib.quote('%s://%s%s' % ( 588 normalized_scheme, normailzed_host, path), safe='~') 589 # TODO: ensure that token escaping logic is correct, not sure if the token 590 # value should be double escaped instead of single. 591 base_string = '&'.join((http_request.method.upper(), request_path, 592 all_parameters)) 593 # Now we have the base string, we can calculate the oauth_signature. 594 return base_string 595 596 597def generate_hmac_signature(http_request, consumer_key, consumer_secret, 598 timestamp, nonce, version, next='oob', 599 token=None, token_secret=None, verifier=None): 600 import hmac 601 import base64 602 base_string = build_oauth_base_string( 603 http_request, consumer_key, nonce, HMAC_SHA1, timestamp, version, 604 next, token, verifier=verifier) 605 hash_key = None 606 hashed = None 607 if token_secret is not None: 608 hash_key = '%s&%s' % (urllib.quote(consumer_secret, safe='~'), 609 urllib.quote(token_secret, safe='~')) 610 else: 611 hash_key = '%s&' % urllib.quote(consumer_secret, safe='~') 612 try: 613 import hashlib 614 hashed = hmac.new(hash_key, base_string, hashlib.sha1) 615 except ImportError: 616 import sha 617 hashed = hmac.new(hash_key, base_string, sha) 618 # Python2.3 does not have base64.b64encode. 619 if hasattr(base64, 'b64encode'): 620 return base64.b64encode(hashed.digest()) 621 else: 622 return base64.encodestring(hashed.digest()).replace('\n', '') 623 624 625def generate_rsa_signature(http_request, consumer_key, rsa_key, 626 timestamp, nonce, version, next='oob', 627 token=None, token_secret=None, verifier=None): 628 import base64 629 try: 630 from tlslite.utils import keyfactory 631 except ImportError: 632 from gdata.tlslite.utils import keyfactory 633 base_string = build_oauth_base_string( 634 http_request, consumer_key, nonce, RSA_SHA1, timestamp, version, 635 next, token, verifier=verifier) 636 private_key = keyfactory.parsePrivateKey(rsa_key) 637 # Sign using the key 638 signed = private_key.hashAndSign(base_string) 639 # Python2.3 does not have base64.b64encode. 640 if hasattr(base64, 'b64encode'): 641 return base64.b64encode(signed) 642 else: 643 return base64.encodestring(signed).replace('\n', '') 644 645 646def generate_auth_header(consumer_key, timestamp, nonce, signature_type, 647 signature, version='1.0', next=None, token=None, 648 verifier=None): 649 """Builds the Authorization header to be sent in the request. 650 651 Args: 652 consumer_key: Identifies the application making the request (str). 653 timestamp: 654 nonce: 655 signature_type: One of either HMAC_SHA1 or RSA_SHA1 656 signature: The HMAC or RSA signature for the request as a base64 657 encoded string. 658 version: The version of the OAuth protocol that this request is using. 659 Default is '1.0' 660 next: The URL of the page that the user's browser should be sent to 661 after they authorize the token. (Optional) 662 token: str The OAuth token value to be used in the oauth_token parameter 663 of the header. 664 verifier: str The OAuth verifier which must be included when you are 665 upgrading a request token to an access token. 666 """ 667 params = { 668 'oauth_consumer_key': consumer_key, 669 'oauth_version': version, 670 'oauth_nonce': nonce, 671 'oauth_timestamp': str(timestamp), 672 'oauth_signature_method': signature_type, 673 'oauth_signature': signature} 674 if next is not None: 675 params['oauth_callback'] = str(next) 676 if token is not None: 677 params['oauth_token'] = token 678 if verifier is not None: 679 params['oauth_verifier'] = verifier 680 pairs = [ 681 '%s="%s"' % ( 682 k, urllib.quote(v, safe='~')) for k, v in params.iteritems()] 683 return 'OAuth %s' % (', '.join(pairs)) 684 685 686REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken' 687ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken' 688 689 690def generate_request_for_request_token( 691 consumer_key, signature_type, scopes, rsa_key=None, consumer_secret=None, 692 auth_server_url=REQUEST_TOKEN_URL, next='oob', version='1.0'): 693 """Creates request to be sent to auth server to get an OAuth request token. 694 695 Args: 696 consumer_key: 697 signature_type: either RSA_SHA1 or HMAC_SHA1. The rsa_key must be 698 provided if the signature type is RSA but if the signature method 699 is HMAC, the consumer_secret must be used. 700 scopes: List of URL prefixes for the data which we want to access. For 701 example, to request access to the user's Blogger and Google Calendar 702 data, we would request 703 ['http://www.blogger.com/feeds/', 704 'https://www.google.com/calendar/feeds/', 705 'http://www.google.com/calendar/feeds/'] 706 rsa_key: Only used if the signature method is RSA_SHA1. 707 consumer_secret: Only used if the signature method is HMAC_SHA1. 708 auth_server_url: The URL to which the token request should be directed. 709 Defaults to 'https://www.google.com/accounts/OAuthGetRequestToken'. 710 next: The URL of the page that the user's browser should be sent to 711 after they authorize the token. (Optional) 712 version: The OAuth version used by the requesting web application. 713 Defaults to '1.0a' 714 715 Returns: 716 An atom.http_core.HttpRequest object with the URL, Authorization header 717 and body filled in. 718 """ 719 request = atom.http_core.HttpRequest(auth_server_url, 'POST') 720 # Add the requested auth scopes to the Auth request URL. 721 if scopes: 722 request.uri.query['scope'] = ' '.join(scopes) 723 724 timestamp = str(int(time.time())) 725 nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) 726 signature = None 727 if signature_type == HMAC_SHA1: 728 signature = generate_hmac_signature( 729 request, consumer_key, consumer_secret, timestamp, nonce, version, 730 next=next) 731 elif signature_type == RSA_SHA1: 732 signature = generate_rsa_signature( 733 request, consumer_key, rsa_key, timestamp, nonce, version, next=next) 734 else: 735 return None 736 737 request.headers['Authorization'] = generate_auth_header( 738 consumer_key, timestamp, nonce, signature_type, signature, version, 739 next) 740 request.headers['Content-Length'] = '0' 741 return request 742 743 744def generate_request_for_access_token( 745 request_token, auth_server_url=ACCESS_TOKEN_URL): 746 """Creates a request to ask the OAuth server for an access token. 747 748 Requires a request token which the user has authorized. See the 749 documentation on OAuth with Google Data for more details: 750 http://code.google.com/apis/accounts/docs/OAuth.html#AccessToken 751 752 Args: 753 request_token: An OAuthHmacToken or OAuthRsaToken which the user has 754 approved using their browser. 755 auth_server_url: (optional) The URL at which the OAuth access token is 756 requested. Defaults to 757 https://www.google.com/accounts/OAuthGetAccessToken 758 759 Returns: 760 A new HttpRequest object which can be sent to the OAuth server to 761 request an OAuth Access Token. 762 """ 763 http_request = atom.http_core.HttpRequest(auth_server_url, 'POST') 764 http_request.headers['Content-Length'] = '0' 765 return request_token.modify_request(http_request) 766 767 768def oauth_token_info_from_body(http_body): 769 """Exracts an OAuth request token from the server's response. 770 771 Returns: 772 A tuple of strings containing the OAuth token and token secret. If 773 neither of these are present in the body, returns (None, None) 774 """ 775 token = None 776 token_secret = None 777 for pair in http_body.split('&'): 778 if pair.startswith('oauth_token='): 779 token = urllib.unquote(pair[len('oauth_token='):]) 780 if pair.startswith('oauth_token_secret='): 781 token_secret = urllib.unquote(pair[len('oauth_token_secret='):]) 782 return (token, token_secret) 783 784 785def hmac_token_from_body(http_body, consumer_key, consumer_secret, 786 auth_state): 787 token_value, token_secret = oauth_token_info_from_body(http_body) 788 token = OAuthHmacToken(consumer_key, consumer_secret, token_value, 789 token_secret, auth_state) 790 return token 791 792 793def rsa_token_from_body(http_body, consumer_key, rsa_private_key, 794 auth_state): 795 token_value, token_secret = oauth_token_info_from_body(http_body) 796 token = OAuthRsaToken(consumer_key, rsa_private_key, token_value, 797 token_secret, auth_state) 798 return token 799 800 801DEFAULT_DOMAIN = 'default' 802OAUTH_AUTHORIZE_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken' 803 804 805def generate_oauth_authorization_url( 806 token, next=None, hd=DEFAULT_DOMAIN, hl=None, btmpl=None, 807 auth_server=OAUTH_AUTHORIZE_URL): 808 """Creates a URL for the page where the request token can be authorized. 809 810 Args: 811 token: str The request token from the OAuth server. 812 next: str (optional) URL the user should be redirected to after granting 813 access to a Google service(s). It can include url-encoded query 814 parameters. 815 hd: str (optional) Identifies a particular hosted domain account to be 816 accessed (for example, 'mycollege.edu'). Uses 'default' to specify a 817 regular Google account ('username@gmail.com'). 818 hl: str (optional) An ISO 639 country code identifying what language the 819 approval page should be translated in (for example, 'hl=en' for 820 English). The default is the user's selected language. 821 btmpl: str (optional) Forces a mobile version of the approval page. The 822 only accepted value is 'mobile'. 823 auth_server: str (optional) The start of the token authorization web 824 page. Defaults to 825 'https://www.google.com/accounts/OAuthAuthorizeToken' 826 827 Returns: 828 An atom.http_core.Uri pointing to the token authorization page where the 829 user may allow or deny this app to access their Google data. 830 """ 831 uri = atom.http_core.Uri.parse_uri(auth_server) 832 uri.query['oauth_token'] = token 833 uri.query['hd'] = hd 834 if next is not None: 835 uri.query['oauth_callback'] = str(next) 836 if hl is not None: 837 uri.query['hl'] = hl 838 if btmpl is not None: 839 uri.query['btmpl'] = btmpl 840 return uri 841 842 843def oauth_token_info_from_url(url): 844 """Exracts an OAuth access token from the redirected page's URL. 845 846 Returns: 847 A tuple of strings containing the OAuth token and the OAuth verifier which 848 need to sent when upgrading a request token to an access token. 849 """ 850 if isinstance(url, (str, unicode)): 851 url = atom.http_core.Uri.parse_uri(url) 852 token = None 853 verifier = None 854 if 'oauth_token' in url.query: 855 token = urllib.unquote(url.query['oauth_token']) 856 if 'oauth_verifier' in url.query: 857 verifier = urllib.unquote(url.query['oauth_verifier']) 858 return (token, verifier) 859 860 861def authorize_request_token(request_token, url): 862 """Adds information to request token to allow it to become an access token. 863 864 Modifies the request_token object passed in by setting and unsetting the 865 necessary fields to allow this token to form a valid upgrade request. 866 867 Args: 868 request_token: The OAuth request token which has been authorized by the 869 user. In order for this token to be upgraded to an access token, 870 certain fields must be extracted from the URL and added to the token 871 so that they can be passed in an upgrade-token request. 872 url: The URL of the current page which the user's browser was redirected 873 to after they authorized access for the app. This function extracts 874 information from the URL which is needed to upgraded the token from 875 a request token to an access token. 876 877 Returns: 878 The same token object which was passed in. 879 """ 880 token, verifier = oauth_token_info_from_url(url) 881 request_token.token = token 882 request_token.verifier = verifier 883 request_token.auth_state = AUTHORIZED_REQUEST_TOKEN 884 return request_token 885 886 887AuthorizeRequestToken = authorize_request_token 888 889 890def upgrade_to_access_token(request_token, server_response_body): 891 """Extracts access token information from response to an upgrade request. 892 893 Once the server has responded with the new token info for the OAuth 894 access token, this method modifies the request_token to set and unset 895 necessary fields to create valid OAuth authorization headers for requests. 896 897 Args: 898 request_token: An OAuth token which this function modifies to allow it 899 to be used as an access token. 900 server_response_body: str The server's response to an OAuthAuthorizeToken 901 request. This should contain the new token and token_secret which 902 are used to generate the signature and parameters of the Authorization 903 header in subsequent requests to Google Data APIs. 904 905 Returns: 906 The same token object which was passed in. 907 """ 908 token, token_secret = oauth_token_info_from_body(server_response_body) 909 request_token.token = token 910 request_token.token_secret = token_secret 911 request_token.auth_state = ACCESS_TOKEN 912 request_token.next = None 913 request_token.verifier = None 914 return request_token 915 916 917UpgradeToAccessToken = upgrade_to_access_token 918 919 920REQUEST_TOKEN = 1 921AUTHORIZED_REQUEST_TOKEN = 2 922ACCESS_TOKEN = 3 923 924 925class OAuthHmacToken(object): 926 SIGNATURE_METHOD = HMAC_SHA1 927 928 def __init__(self, consumer_key, consumer_secret, token, token_secret, 929 auth_state, next=None, verifier=None): 930 self.consumer_key = consumer_key 931 self.consumer_secret = consumer_secret 932 self.token = token 933 self.token_secret = token_secret 934 self.auth_state = auth_state 935 self.next = next 936 self.verifier = verifier # Used to convert request token to access token. 937 938 def generate_authorization_url( 939 self, google_apps_domain=DEFAULT_DOMAIN, language=None, btmpl=None, 940 auth_server=OAUTH_AUTHORIZE_URL): 941 """Creates the URL at which the user can authorize this app to access. 942 943 Args: 944 google_apps_domain: str (optional) If the user should be signing in 945 using an account under a known Google Apps domain, provide the 946 domain name ('example.com') here. If not provided, 'default' 947 will be used, and the user will be prompted to select an account 948 if they are signed in with a Google Account and Google Apps 949 accounts. 950 language: str (optional) An ISO 639 country code identifying what 951 language the approval page should be translated in (for example, 952 'en' for English). The default is the user's selected language. 953 btmpl: str (optional) Forces a mobile version of the approval page. The 954 only accepted value is 'mobile'. 955 auth_server: str (optional) The start of the token authorization web 956 page. Defaults to 957 'https://www.google.com/accounts/OAuthAuthorizeToken' 958 """ 959 return generate_oauth_authorization_url( 960 self.token, hd=google_apps_domain, hl=language, btmpl=btmpl, 961 auth_server=auth_server) 962 963 GenerateAuthorizationUrl = generate_authorization_url 964 965 def modify_request(self, http_request): 966 """Sets the Authorization header in the HTTP request using the token. 967 968 Calculates an HMAC signature using the information in the token to 969 indicate that the request came from this application and that this 970 application has permission to access a particular user's data. 971 972 Returns: 973 The same HTTP request object which was passed in. 974 """ 975 timestamp = str(int(time.time())) 976 nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) 977 signature = generate_hmac_signature( 978 http_request, self.consumer_key, self.consumer_secret, timestamp, 979 nonce, version='1.0', next=self.next, token=self.token, 980 token_secret=self.token_secret, verifier=self.verifier) 981 http_request.headers['Authorization'] = generate_auth_header( 982 self.consumer_key, timestamp, nonce, HMAC_SHA1, signature, 983 version='1.0', next=self.next, token=self.token, 984 verifier=self.verifier) 985 return http_request 986 987 ModifyRequest = modify_request 988 989 990class OAuthRsaToken(OAuthHmacToken): 991 SIGNATURE_METHOD = RSA_SHA1 992 993 def __init__(self, consumer_key, rsa_private_key, token, token_secret, 994 auth_state, next=None, verifier=None): 995 self.consumer_key = consumer_key 996 self.rsa_private_key = rsa_private_key 997 self.token = token 998 self.token_secret = token_secret 999 self.auth_state = auth_state 1000 self.next = next 1001 self.verifier = verifier # Used to convert request token to access token. 1002 1003 def modify_request(self, http_request): 1004 """Sets the Authorization header in the HTTP request using the token. 1005 1006 Calculates an RSA signature using the information in the token to 1007 indicate that the request came from this application and that this 1008 application has permission to access a particular user's data. 1009 1010 Returns: 1011 The same HTTP request object which was passed in. 1012 """ 1013 timestamp = str(int(time.time())) 1014 nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)]) 1015 signature = generate_rsa_signature( 1016 http_request, self.consumer_key, self.rsa_private_key, timestamp, 1017 nonce, version='1.0', next=self.next, token=self.token, 1018 token_secret=self.token_secret, verifier=self.verifier) 1019 http_request.headers['Authorization'] = generate_auth_header( 1020 self.consumer_key, timestamp, nonce, RSA_SHA1, signature, 1021 version='1.0', next=self.next, token=self.token, 1022 verifier=self.verifier) 1023 return http_request 1024 1025 ModifyRequest = modify_request 1026 1027 1028class TwoLeggedOAuthHmacToken(OAuthHmacToken): 1029 1030 def __init__(self, consumer_key, consumer_secret, requestor_id): 1031 self.requestor_id = requestor_id 1032 OAuthHmacToken.__init__( 1033 self, consumer_key, consumer_secret, None, None, ACCESS_TOKEN, 1034 next=None, verifier=None) 1035 1036 def modify_request(self, http_request): 1037 """Sets the Authorization header in the HTTP request using the token. 1038 1039 Calculates an HMAC signature using the information in the token to 1040 indicate that the request came from this application and that this 1041 application has permission to access a particular user's data using 2LO. 1042 1043 Returns: 1044 The same HTTP request object which was passed in. 1045 """ 1046 http_request.uri.query['xoauth_requestor_id'] = self.requestor_id 1047 return OAuthHmacToken.modify_request(self, http_request) 1048 1049 ModifyRequest = modify_request 1050 1051 1052class TwoLeggedOAuthRsaToken(OAuthRsaToken): 1053 1054 def __init__(self, consumer_key, rsa_private_key, requestor_id): 1055 self.requestor_id = requestor_id 1056 OAuthRsaToken.__init__( 1057 self, consumer_key, rsa_private_key, None, None, ACCESS_TOKEN, 1058 next=None, verifier=None) 1059 1060 def modify_request(self, http_request): 1061 """Sets the Authorization header in the HTTP request using the token. 1062 1063 Calculates an RSA signature using the information in the token to 1064 indicate that the request came from this application and that this 1065 application has permission to access a particular user's data using 2LO. 1066 1067 Returns: 1068 The same HTTP request object which was passed in. 1069 """ 1070 http_request.uri.query['xoauth_requestor_id'] = self.requestor_id 1071 return OAuthRsaToken.modify_request(self, http_request) 1072 1073 ModifyRequest = modify_request 1074 1075 1076def _join_token_parts(*args): 1077 """"Escapes and combines all strings passed in. 1078 1079 Used to convert a token object's members into a string instead of 1080 using pickle. 1081 1082 Note: A None value will be converted to an empty string. 1083 1084 Returns: 1085 A string in the form 1x|member1|member2|member3... 1086 """ 1087 return '|'.join([urllib.quote_plus(a or '') for a in args]) 1088 1089 1090def _split_token_parts(blob): 1091 """Extracts and unescapes fields from the provided binary string. 1092 1093 Reverses the packing performed by _join_token_parts. Used to extract 1094 the members of a token object. 1095 1096 Note: An empty string from the blob will be interpreted as None. 1097 1098 Args: 1099 blob: str A string of the form 1x|member1|member2|member3 as created 1100 by _join_token_parts 1101 1102 Returns: 1103 A list of unescaped strings. 1104 """ 1105 return [urllib.unquote_plus(part) or None for part in blob.split('|')] 1106 1107 1108def token_to_blob(token): 1109 """Serializes the token data as a string for storage in a datastore. 1110 1111 Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken, 1112 OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken, 1113 TwoLeggedOAuthHmacToken. 1114 1115 Args: 1116 token: A token object which must be of one of the supported token classes. 1117 1118 Raises: 1119 UnsupportedTokenType if the token is not one of the supported token 1120 classes listed above. 1121 1122 Returns: 1123 A string represenging this token. The string can be converted back into 1124 an equivalent token object using token_from_blob. Note that any members 1125 which are set to '' will be set to None when the token is deserialized 1126 by token_from_blob. 1127 """ 1128 if isinstance(token, ClientLoginToken): 1129 return _join_token_parts('1c', token.token_string) 1130 # Check for secure auth sub type first since it is a subclass of 1131 # AuthSubToken. 1132 elif isinstance(token, SecureAuthSubToken): 1133 return _join_token_parts('1s', token.token_string, token.rsa_private_key, 1134 *token.scopes) 1135 elif isinstance(token, AuthSubToken): 1136 return _join_token_parts('1a', token.token_string, *token.scopes) 1137 elif isinstance(token, TwoLeggedOAuthRsaToken): 1138 return _join_token_parts( 1139 '1rtl', token.consumer_key, token.rsa_private_key, token.requestor_id) 1140 elif isinstance(token, TwoLeggedOAuthHmacToken): 1141 return _join_token_parts( 1142 '1htl', token.consumer_key, token.consumer_secret, token.requestor_id) 1143 # Check RSA OAuth token first since the OAuthRsaToken is a subclass of 1144 # OAuthHmacToken. 1145 elif isinstance(token, OAuthRsaToken): 1146 return _join_token_parts( 1147 '1r', token.consumer_key, token.rsa_private_key, token.token, 1148 token.token_secret, str(token.auth_state), token.next, 1149 token.verifier) 1150 elif isinstance(token, OAuthHmacToken): 1151 return _join_token_parts( 1152 '1h', token.consumer_key, token.consumer_secret, token.token, 1153 token.token_secret, str(token.auth_state), token.next, 1154 token.verifier) 1155 else: 1156 raise UnsupportedTokenType( 1157 'Unable to serialize token of type %s' % type(token)) 1158 1159 1160TokenToBlob = token_to_blob 1161 1162 1163def token_from_blob(blob): 1164 """Deserializes a token string from the datastore back into a token object. 1165 1166 Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken, 1167 OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken, 1168 TwoLeggedOAuthHmacToken. 1169 1170 Args: 1171 blob: string created by token_to_blob. 1172 1173 Raises: 1174 UnsupportedTokenType if the token is not one of the supported token 1175 classes listed above. 1176 1177 Returns: 1178 A new token object with members set to the values serialized in the 1179 blob string. Note that any members which were set to '' in the original 1180 token will now be None. 1181 """ 1182 parts = _split_token_parts(blob) 1183 if parts[0] == '1c': 1184 return ClientLoginToken(parts[1]) 1185 elif parts[0] == '1a': 1186 return AuthSubToken(parts[1], parts[2:]) 1187 elif parts[0] == '1s': 1188 return SecureAuthSubToken(parts[1], parts[2], parts[3:]) 1189 elif parts[0] == '1rtl': 1190 return TwoLeggedOAuthRsaToken(parts[1], parts[2], parts[3]) 1191 elif parts[0] == '1htl': 1192 return TwoLeggedOAuthHmacToken(parts[1], parts[2], parts[3]) 1193 elif parts[0] == '1r': 1194 auth_state = int(parts[5]) 1195 return OAuthRsaToken(parts[1], parts[2], parts[3], parts[4], auth_state, 1196 parts[6], parts[7]) 1197 elif parts[0] == '1h': 1198 auth_state = int(parts[5]) 1199 return OAuthHmacToken(parts[1], parts[2], parts[3], parts[4], auth_state, 1200 parts[6], parts[7]) 1201 else: 1202 raise UnsupportedTokenType( 1203 'Unable to deserialize token with type marker of %s' % parts[0]) 1204 1205 1206TokenFromBlob = token_from_blob 1207 1208 1209def dump_tokens(tokens): 1210 return ','.join([token_to_blob(t) for t in tokens]) 1211 1212 1213def load_tokens(blob): 1214 return [token_from_blob(s) for s in blob.split(',')] 1215 1216 1217def find_scopes_for_services(service_names=None): 1218 """Creates a combined list of scope URLs for the desired services. 1219 1220 This method searches the AUTH_SCOPES dictionary. 1221 1222 Args: 1223 service_names: list of strings (optional) Each name must be a key in the 1224 AUTH_SCOPES dictionary. If no list is provided (None) then 1225 the resulting list will contain all scope URLs in the 1226 AUTH_SCOPES dict. 1227 1228 Returns: 1229 A list of URL strings which are the scopes needed to access these services 1230 when requesting a token using AuthSub or OAuth. 1231 """ 1232 result_scopes = [] 1233 if service_names is None: 1234 for service_name, scopes in AUTH_SCOPES.iteritems(): 1235 result_scopes.extend(scopes) 1236 else: 1237 for service_name in service_names: 1238 result_scopes.extend(AUTH_SCOPES[service_name]) 1239 return result_scopes 1240 1241 1242FindScopesForServices = find_scopes_for_services 1243 1244 1245def ae_save(token, token_key): 1246 """Stores an auth token in the App Engine datastore. 1247 1248 This is a convenience method for using the library with App Engine. 1249 Recommended usage is to associate the auth token with the current_user. 1250 If a user is signed in to the app using the App Engine users API, you 1251 can use 1252 gdata.gauth.ae_save(some_token, users.get_current_user().user_id()) 1253 If you are not using the Users API you are free to choose whatever 1254 string you would like for a token_string. 1255 1256 Args: 1257 token: an auth token object. Must be one of ClientLoginToken, 1258 AuthSubToken, SecureAuthSubToken, OAuthRsaToken, or OAuthHmacToken 1259 (see token_to_blob). 1260 token_key: str A unique identified to be used when you want to retrieve 1261 the token. If the user is signed in to App Engine using the 1262 users API, I recommend using the user ID for the token_key: 1263 users.get_current_user().user_id() 1264 """ 1265 import gdata.alt.app_engine 1266 key_name = ''.join(('gd_auth_token', token_key)) 1267 return gdata.alt.app_engine.set_token(key_name, token_to_blob(token)) 1268 1269 1270AeSave = ae_save 1271 1272 1273def ae_load(token_key): 1274 """Retrieves a token object from the App Engine datastore. 1275 1276 This is a convenience method for using the library with App Engine. 1277 See also ae_save. 1278 1279 Args: 1280 token_key: str The unique key associated with the desired token when it 1281 was saved using ae_save. 1282 1283 Returns: 1284 A token object if there was a token associated with the token_key or None 1285 if the key could not be found. 1286 """ 1287 import gdata.alt.app_engine 1288 key_name = ''.join(('gd_auth_token', token_key)) 1289 token_string = gdata.alt.app_engine.get_token(key_name) 1290 if token_string is not None: 1291 return token_from_blob(token_string) 1292 else: 1293 return None 1294 1295 1296AeLoad = ae_load 1297 1298 1299def ae_delete(token_key): 1300 """Removes the token object from the App Engine datastore.""" 1301 import gdata.alt.app_engine 1302 key_name = ''.join(('gd_auth_token', token_key)) 1303 gdata.alt.app_engine.delete_token(key_name) 1304 1305 1306AeDelete = ae_delete