PageRenderTime 70ms CodeModel.GetById 13ms app.highlight 46ms RepoModel.GetById 1ms app.codeStats 1ms

/gdata/gauth.py

http://radioappz.googlecode.com/
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