PageRenderTime 59ms CodeModel.GetById 10ms app.highlight 40ms RepoModel.GetById 2ms app.codeStats 0ms

/gdata/auth.py

http://radioappz.googlecode.com/
Python | 952 lines | 861 code | 20 blank | 71 comment | 36 complexity | 7650c25637770e4937a639a3e0618640 MD5 | raw file
  1#!/usr/bin/python
  2#
  3# Copyright (C) 2007 - 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
 18import cgi
 19import math
 20import random
 21import re
 22import time
 23import types
 24import urllib
 25import atom.http_interface
 26import atom.token_store
 27import atom.url
 28import gdata.oauth as oauth
 29import gdata.oauth.rsa as oauth_rsa
 30import gdata.tlslite.utils.keyfactory as keyfactory
 31import gdata.tlslite.utils.cryptomath as cryptomath
 32
 33import gdata.gauth
 34
 35__author__ = 'api.jscudder (Jeff Scudder)'
 36
 37
 38PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
 39AUTHSUB_AUTH_LABEL = 'AuthSub token='
 40
 41
 42"""This module provides functions and objects used with Google authentication.
 43
 44Details on Google authorization mechanisms used with the Google Data APIs can
 45be found here: 
 46http://code.google.com/apis/gdata/auth.html
 47http://code.google.com/apis/accounts/
 48
 49The essential functions are the following.
 50Related to ClientLogin:
 51  generate_client_login_request_body: Constructs the body of an HTTP request to
 52                                      obtain a ClientLogin token for a specific
 53                                      service. 
 54  extract_client_login_token: Creates a ClientLoginToken with the token from a
 55                              success response to a ClientLogin request.
 56  get_captcha_challenge: If the server responded to the ClientLogin request
 57                         with a CAPTCHA challenge, this method extracts the
 58                         CAPTCHA URL and identifying CAPTCHA token.
 59
 60Related to AuthSub:
 61  generate_auth_sub_url: Constructs a full URL for a AuthSub request. The 
 62                         user's browser must be sent to this Google Accounts
 63                         URL and redirected back to the app to obtain the
 64                         AuthSub token.
 65  extract_auth_sub_token_from_url: Once the user's browser has been 
 66                                   redirected back to the web app, use this
 67                                   function to create an AuthSubToken with
 68                                   the correct authorization token and scope.
 69  token_from_http_body: Extracts the AuthSubToken value string from the 
 70                        server's response to an AuthSub session token upgrade
 71                        request.
 72"""
 73
 74def generate_client_login_request_body(email, password, service, source, 
 75    account_type='HOSTED_OR_GOOGLE', captcha_token=None, 
 76    captcha_response=None):
 77  """Creates the body of the autentication request
 78
 79  See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request
 80  for more details.
 81
 82  Args:
 83    email: str
 84    password: str
 85    service: str
 86    source: str
 87    account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid
 88        values are 'GOOGLE' and 'HOSTED'
 89    captcha_token: str (optional)
 90    captcha_response: str (optional)
 91
 92  Returns:
 93    The HTTP body to send in a request for a client login token.
 94  """
 95  return gdata.gauth.generate_client_login_request_body(email, password,
 96      service, source, account_type, captcha_token, captcha_response)
 97
 98
 99GenerateClientLoginRequestBody = generate_client_login_request_body
100
101
102def GenerateClientLoginAuthToken(http_body):
103  """Returns the token value to use in Authorization headers.
104
105  Reads the token from the server's response to a Client Login request and
106  creates header value to use in requests.
107
108  Args:
109    http_body: str The body of the server's HTTP response to a Client Login
110        request
111 
112  Returns:
113    The value half of an Authorization header.
114  """
115  token = get_client_login_token(http_body)
116  if token:
117    return 'GoogleLogin auth=%s' % token
118  return None
119
120
121def get_client_login_token(http_body):
122  """Returns the token value for a ClientLoginToken.
123
124  Reads the token from the server's response to a Client Login request and
125  creates the token value string to use in requests.
126
127  Args:
128    http_body: str The body of the server's HTTP response to a Client Login
129        request
130 
131  Returns:
132    The token value string for a ClientLoginToken.
133  """
134  return gdata.gauth.get_client_login_token_string(http_body)
135
136
137def extract_client_login_token(http_body, scopes):
138  """Parses the server's response and returns a ClientLoginToken.
139  
140  Args:
141    http_body: str The body of the server's HTTP response to a Client Login
142               request. It is assumed that the login request was successful.
143    scopes: list containing atom.url.Urls or strs. The scopes list contains
144            all of the partial URLs under which the client login token is
145            valid. For example, if scopes contains ['http://example.com/foo']
146            then the client login token would be valid for 
147            http://example.com/foo/bar/baz
148
149  Returns:
150    A ClientLoginToken which is valid for the specified scopes.
151  """
152  token_string = get_client_login_token(http_body)
153  token = ClientLoginToken(scopes=scopes)
154  token.set_token_string(token_string)
155  return token
156
157
158def get_captcha_challenge(http_body, 
159    captcha_base_url='http://www.google.com/accounts/'):
160  """Returns the URL and token for a CAPTCHA challenge issued by the server.
161
162  Args:
163    http_body: str The body of the HTTP response from the server which 
164        contains the CAPTCHA challenge.
165    captcha_base_url: str This function returns a full URL for viewing the 
166        challenge image which is built from the server's response. This
167        base_url is used as the beginning of the URL because the server
168        only provides the end of the URL. For example the server provides
169        'Captcha?ctoken=Hi...N' and the URL for the image is
170        'http://www.google.com/accounts/Captcha?ctoken=Hi...N'
171
172  Returns:
173    A dictionary containing the information needed to repond to the CAPTCHA
174    challenge, the image URL and the ID token of the challenge. The 
175    dictionary is in the form:
176    {'token': string identifying the CAPTCHA image,
177     'url': string containing the URL of the image}
178    Returns None if there was no CAPTCHA challenge in the response.
179  """
180  return gdata.gauth.get_captcha_challenge(http_body, captcha_base_url)
181
182
183GetCaptchaChallenge = get_captcha_challenge
184
185
186def GenerateOAuthRequestTokenUrl(
187    oauth_input_params, scopes,
188    request_token_url='https://www.google.com/accounts/OAuthGetRequestToken',
189    extra_parameters=None):
190  """Generate a URL at which a request for OAuth request token is to be sent.
191  
192  Args:
193    oauth_input_params: OAuthInputParams OAuth input parameters.
194    scopes: list of strings The URLs of the services to be accessed.
195    request_token_url: string The beginning of the request token URL. This is
196        normally 'https://www.google.com/accounts/OAuthGetRequestToken' or
197        '/accounts/OAuthGetRequestToken'
198    extra_parameters: dict (optional) key-value pairs as any additional
199        parameters to be included in the URL and signature while making a
200        request for fetching an OAuth request token. All the OAuth parameters
201        are added by default. But if provided through this argument, any
202        default parameters will be overwritten. For e.g. a default parameter
203        oauth_version 1.0 can be overwritten if
204        extra_parameters = {'oauth_version': '2.0'}
205  
206  Returns:
207    atom.url.Url OAuth request token URL.
208  """
209  scopes_string = ' '.join([str(scope) for scope in scopes])
210  parameters = {'scope': scopes_string}
211  if extra_parameters:
212    parameters.update(extra_parameters)
213  oauth_request = oauth.OAuthRequest.from_consumer_and_token(
214      oauth_input_params.GetConsumer(), http_url=request_token_url,
215      parameters=parameters)
216  oauth_request.sign_request(oauth_input_params.GetSignatureMethod(),
217                             oauth_input_params.GetConsumer(), None)
218  return atom.url.parse_url(oauth_request.to_url())
219
220
221def GenerateOAuthAuthorizationUrl(
222    request_token,
223    authorization_url='https://www.google.com/accounts/OAuthAuthorizeToken',
224    callback_url=None, extra_params=None,
225    include_scopes_in_callback=False, scopes_param_prefix='oauth_token_scope'):
226  """Generates URL at which user will login to authorize the request token.
227  
228  Args:
229    request_token: gdata.auth.OAuthToken OAuth request token.
230    authorization_url: string The beginning of the authorization URL. This is
231        normally 'https://www.google.com/accounts/OAuthAuthorizeToken' or
232        '/accounts/OAuthAuthorizeToken'
233    callback_url: string (optional) The URL user will be sent to after
234        logging in and granting access.
235    extra_params: dict (optional) Additional parameters to be sent.
236    include_scopes_in_callback: Boolean (default=False) if set to True, and
237        if 'callback_url' is present, the 'callback_url' will be modified to
238        include the scope(s) from the request token as a URL parameter. The
239        key for the 'callback' URL's scope parameter will be
240        OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as
241        a parameter to the 'callback' URL, is that the page which receives
242        the OAuth token will be able to tell which URLs the token grants
243        access to.
244    scopes_param_prefix: string (default='oauth_token_scope') The URL
245        parameter key which maps to the list of valid scopes for the token.
246        This URL parameter will be included in the callback URL along with
247        the scopes of the token as value if include_scopes_in_callback=True.
248
249  Returns:
250    atom.url.Url OAuth authorization URL.
251  """
252  scopes = request_token.scopes
253  if isinstance(scopes, list):
254    scopes = ' '.join(scopes)  
255  if include_scopes_in_callback and callback_url:
256    if callback_url.find('?') > -1:
257      callback_url += '&'
258    else:
259      callback_url += '?'
260    callback_url += urllib.urlencode({scopes_param_prefix:scopes})  
261  oauth_token = oauth.OAuthToken(request_token.key, request_token.secret)
262  oauth_request = oauth.OAuthRequest.from_token_and_callback(
263      token=oauth_token, callback=callback_url,
264      http_url=authorization_url, parameters=extra_params)
265  return atom.url.parse_url(oauth_request.to_url())
266
267
268def GenerateOAuthAccessTokenUrl(
269    authorized_request_token,
270    oauth_input_params,
271    access_token_url='https://www.google.com/accounts/OAuthGetAccessToken',
272    oauth_version='1.0',
273    oauth_verifier=None):
274  """Generates URL at which user will login to authorize the request token.
275  
276  Args:
277    authorized_request_token: gdata.auth.OAuthToken OAuth authorized request
278        token.
279    oauth_input_params: OAuthInputParams OAuth input parameters.    
280    access_token_url: string The beginning of the authorization URL. This is
281        normally 'https://www.google.com/accounts/OAuthGetAccessToken' or
282        '/accounts/OAuthGetAccessToken'
283    oauth_version: str (default='1.0') oauth_version parameter.
284    oauth_verifier: str (optional) If present, it is assumed that the client
285        will use the OAuth v1.0a protocol which includes passing the
286        oauth_verifier (as returned by the SP) in the access token step.
287
288  Returns:
289    atom.url.Url OAuth access token URL.
290  """
291  oauth_token = oauth.OAuthToken(authorized_request_token.key,
292                                 authorized_request_token.secret)
293  parameters = {'oauth_version': oauth_version}
294  if oauth_verifier is not None:
295    parameters['oauth_verifier'] = oauth_verifier
296  oauth_request = oauth.OAuthRequest.from_consumer_and_token(
297      oauth_input_params.GetConsumer(), token=oauth_token,
298      http_url=access_token_url, parameters=parameters)
299  oauth_request.sign_request(oauth_input_params.GetSignatureMethod(),
300                             oauth_input_params.GetConsumer(), oauth_token)
301  return atom.url.parse_url(oauth_request.to_url())
302
303
304def GenerateAuthSubUrl(next, scope, secure=False, session=True, 
305    request_url='https://www.google.com/accounts/AuthSubRequest',
306    domain='default'):
307  """Generate a URL at which the user will login and be redirected back.
308
309  Users enter their credentials on a Google login page and a token is sent
310  to the URL specified in next. See documentation for AuthSub login at:
311  http://code.google.com/apis/accounts/AuthForWebApps.html
312
313  Args:
314    request_url: str The beginning of the request URL. This is normally
315        'http://www.google.com/accounts/AuthSubRequest' or 
316        '/accounts/AuthSubRequest'
317    next: string The URL user will be sent to after logging in.
318    scope: string The URL of the service to be accessed.
319    secure: boolean (optional) Determines whether or not the issued token
320            is a secure token.
321    session: boolean (optional) Determines whether or not the issued token
322             can be upgraded to a session token.
323    domain: str (optional) The Google Apps domain for this account. If this
324            is not a Google Apps account, use 'default' which is the default
325            value.
326  """
327  # Translate True/False values for parameters into numeric values acceoted
328  # by the AuthSub service.
329  if secure:
330    secure = 1
331  else:
332    secure = 0
333
334  if session:
335    session = 1
336  else:
337    session = 0
338
339  request_params = urllib.urlencode({'next': next, 'scope': scope,
340                                     'secure': secure, 'session': session, 
341                                     'hd': domain})
342  if request_url.find('?') == -1:
343    return '%s?%s' % (request_url, request_params)
344  else:
345    # The request URL already contained url parameters so we should add
346    # the parameters using the & seperator
347    return '%s&%s' % (request_url, request_params)
348
349
350def generate_auth_sub_url(next, scopes, secure=False, session=True,
351    request_url='https://www.google.com/accounts/AuthSubRequest', 
352    domain='default', scopes_param_prefix='auth_sub_scopes'):
353  """Constructs a URL string for requesting a multiscope AuthSub token.
354
355  The generated token will contain a URL parameter to pass along the 
356  requested scopes to the next URL. When the Google Accounts page 
357  redirects the broswser to the 'next' URL, it appends the single use
358  AuthSub token value to the URL as a URL parameter with the key 'token'.
359  However, the information about which scopes were requested is not
360  included by Google Accounts. This method adds the scopes to the next
361  URL before making the request so that the redirect will be sent to 
362  a page, and both the token value and the list of scopes can be 
363  extracted from the request URL. 
364
365  Args:
366    next: atom.url.URL or string The URL user will be sent to after
367          authorizing this web application to access their data.
368    scopes: list containint strings The URLs of the services to be accessed.
369    secure: boolean (optional) Determines whether or not the issued token
370            is a secure token.
371    session: boolean (optional) Determines whether or not the issued token
372             can be upgraded to a session token.
373    request_url: atom.url.Url or str The beginning of the request URL. This
374        is normally 'http://www.google.com/accounts/AuthSubRequest' or 
375        '/accounts/AuthSubRequest'
376    domain: The domain which the account is part of. This is used for Google
377        Apps accounts, the default value is 'default' which means that the
378        requested account is a Google Account (@gmail.com for example)
379    scopes_param_prefix: str (optional) The requested scopes are added as a 
380        URL parameter to the next URL so that the page at the 'next' URL can
381        extract the token value and the valid scopes from the URL. The key
382        for the URL parameter defaults to 'auth_sub_scopes'
383
384  Returns:
385    An atom.url.Url which the user's browser should be directed to in order
386    to authorize this application to access their information.
387  """
388  if isinstance(next, (str, unicode)):
389    next = atom.url.parse_url(next)
390  scopes_string = ' '.join([str(scope) for scope in scopes])
391  next.params[scopes_param_prefix] = scopes_string
392
393  if isinstance(request_url, (str, unicode)):
394    request_url = atom.url.parse_url(request_url)
395  request_url.params['next'] = str(next)
396  request_url.params['scope'] = scopes_string
397  if session:
398    request_url.params['session'] = 1
399  else:
400    request_url.params['session'] = 0
401  if secure:
402    request_url.params['secure'] = 1
403  else:
404    request_url.params['secure'] = 0
405  request_url.params['hd'] = domain
406  return request_url
407
408
409def AuthSubTokenFromUrl(url):
410  """Extracts the AuthSub token from the URL. 
411
412  Used after the AuthSub redirect has sent the user to the 'next' page and
413  appended the token to the URL. This function returns the value to be used
414  in the Authorization header. 
415
416  Args:
417    url: str The URL of the current page which contains the AuthSub token as
418        a URL parameter.
419  """
420  token = TokenFromUrl(url)
421  if token:
422    return 'AuthSub token=%s' % token
423  return None
424
425
426def TokenFromUrl(url):
427  """Extracts the AuthSub token from the URL.
428
429  Returns the raw token value.
430
431  Args:
432    url: str The URL or the query portion of the URL string (after the ?) of
433        the current page which contains the AuthSub token as a URL parameter.
434  """
435  if url.find('?') > -1:
436    query_params = url.split('?')[1]
437  else:
438    query_params = url
439  for pair in query_params.split('&'):
440    if pair.startswith('token='):
441      return pair[6:]
442  return None
443
444
445def extract_auth_sub_token_from_url(url, 
446    scopes_param_prefix='auth_sub_scopes', rsa_key=None):
447  """Creates an AuthSubToken and sets the token value and scopes from the URL.
448  
449  After the Google Accounts AuthSub pages redirect the user's broswer back to 
450  the web application (using the 'next' URL from the request) the web app must
451  extract the token from the current page's URL. The token is provided as a 
452  URL parameter named 'token' and if generate_auth_sub_url was used to create
453  the request, the token's valid scopes are included in a URL parameter whose
454  name is specified in scopes_param_prefix.
455
456  Args:
457    url: atom.url.Url or str representing the current URL. The token value
458         and valid scopes should be included as URL parameters.
459    scopes_param_prefix: str (optional) The URL parameter key which maps to
460                         the list of valid scopes for the token.
461
462  Returns:
463    An AuthSubToken with the token value from the URL and set to be valid for
464    the scopes passed in on the URL. If no scopes were included in the URL,
465    the AuthSubToken defaults to being valid for no scopes. If there was no
466    'token' parameter in the URL, this function returns None.
467  """
468  if isinstance(url, (str, unicode)):
469    url = atom.url.parse_url(url)
470  if 'token' not in url.params:
471    return None
472  scopes = []
473  if scopes_param_prefix in url.params:
474    scopes = url.params[scopes_param_prefix].split(' ')
475  token_value = url.params['token']
476  if rsa_key:
477    token = SecureAuthSubToken(rsa_key, scopes=scopes)
478  else:
479    token = AuthSubToken(scopes=scopes)
480  token.set_token_string(token_value)
481  return token
482
483
484def AuthSubTokenFromHttpBody(http_body):
485  """Extracts the AuthSub token from an HTTP body string.
486
487  Used to find the new session token after making a request to upgrade a
488  single use AuthSub token.
489
490  Args:
491    http_body: str The repsonse from the server which contains the AuthSub
492        key. For example, this function would find the new session token
493        from the server's response to an upgrade token request.
494
495  Returns:
496    The header value to use for Authorization which contains the AuthSub
497    token.
498  """
499  token_value = token_from_http_body(http_body)
500  if token_value:
501    return '%s%s' % (AUTHSUB_AUTH_LABEL, token_value)
502  return None
503
504
505def token_from_http_body(http_body):
506  """Extracts the AuthSub token from an HTTP body string.
507
508  Used to find the new session token after making a request to upgrade a 
509  single use AuthSub token.
510
511  Args:
512    http_body: str The repsonse from the server which contains the AuthSub 
513        key. For example, this function would find the new session token
514        from the server's response to an upgrade token request.
515
516  Returns:
517    The raw token value to use in an AuthSubToken object.
518  """
519  for response_line in http_body.splitlines():
520    if response_line.startswith('Token='):
521      # Strip off Token= and return the token value string.
522      return response_line[6:]
523  return None
524
525
526TokenFromHttpBody = token_from_http_body
527
528
529def OAuthTokenFromUrl(url, scopes_param_prefix='oauth_token_scope'):
530  """Creates an OAuthToken and sets token key and scopes (if present) from URL.
531  
532  After the Google Accounts OAuth pages redirect the user's broswer back to 
533  the web application (using the 'callback' URL from the request) the web app
534  can extract the token from the current page's URL. The token is same as the
535  request token, but it is either authorized (if user grants access) or
536  unauthorized (if user denies access). The token is provided as a 
537  URL parameter named 'oauth_token' and if it was chosen to use
538  GenerateOAuthAuthorizationUrl with include_scopes_in_param=True, the token's
539  valid scopes are included in a URL parameter whose name is specified in
540  scopes_param_prefix.
541
542  Args:
543    url: atom.url.Url or str representing the current URL. The token value
544        and valid scopes should be included as URL parameters.
545    scopes_param_prefix: str (optional) The URL parameter key which maps to
546        the list of valid scopes for the token.
547
548  Returns:
549    An OAuthToken with the token key from the URL and set to be valid for
550    the scopes passed in on the URL. If no scopes were included in the URL,
551    the OAuthToken defaults to being valid for no scopes. If there was no
552    'oauth_token' parameter in the URL, this function returns None.
553  """
554  if isinstance(url, (str, unicode)):
555    url = atom.url.parse_url(url)
556  if 'oauth_token' not in url.params:
557    return None
558  scopes = []
559  if scopes_param_prefix in url.params:
560    scopes = url.params[scopes_param_prefix].split(' ')
561  token_key = url.params['oauth_token']
562  token = OAuthToken(key=token_key, scopes=scopes)
563  return token
564
565
566def OAuthTokenFromHttpBody(http_body):
567  """Parses the HTTP response body and returns an OAuth token.
568  
569  The returned OAuth token will just have key and secret parameters set.
570  It won't have any knowledge about the scopes or oauth_input_params. It is
571  your responsibility to make it aware of the remaining parameters.
572  
573  Returns:
574    OAuthToken OAuth token.
575  """
576  token = oauth.OAuthToken.from_string(http_body)
577  oauth_token = OAuthToken(key=token.key, secret=token.secret)
578  return oauth_token
579  
580
581class OAuthSignatureMethod(object):
582  """Holds valid OAuth signature methods.
583  
584  RSA_SHA1: Class to build signature according to RSA-SHA1 algorithm.
585  HMAC_SHA1: Class to build signature according to HMAC-SHA1 algorithm.
586  """
587  
588  HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1  
589  
590  class RSA_SHA1(oauth_rsa.OAuthSignatureMethod_RSA_SHA1):
591    """Provides implementation for abstract methods to return RSA certs."""
592
593    def __init__(self, private_key, public_cert):
594      self.private_key = private_key
595      self.public_cert = public_cert
596  
597    def _fetch_public_cert(self, unused_oauth_request):
598      return self.public_cert
599  
600    def _fetch_private_cert(self, unused_oauth_request):
601      return self.private_key
602  
603
604class OAuthInputParams(object):
605  """Stores OAuth input parameters.
606  
607  This class is a store for OAuth input parameters viz. consumer key and secret,
608  signature method and RSA key.
609  """
610  
611  def __init__(self, signature_method, consumer_key, consumer_secret=None,
612               rsa_key=None, requestor_id=None):
613    """Initializes object with parameters required for using OAuth mechanism.
614    
615    NOTE: Though consumer_secret and rsa_key are optional, either of the two
616    is required depending on the value of the signature_method.
617    
618    Args:
619      signature_method: class which provides implementation for strategy class
620          oauth.oauth.OAuthSignatureMethod. Signature method to be used for
621          signing each request. Valid implementations are provided as the
622          constants defined by gdata.auth.OAuthSignatureMethod. Currently
623          they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and
624          gdata.auth.OAuthSignatureMethod.HMAC_SHA1. Instead of passing in
625          the strategy class, you may pass in a string for 'RSA_SHA1' or 
626          'HMAC_SHA1'. If you plan to use OAuth on App Engine (or another
627          WSGI environment) I recommend specifying signature method using a
628          string (the only options are 'RSA_SHA1' and 'HMAC_SHA1'). In these
629          environments there are sometimes issues with pickling an object in 
630          which a member references a class or function. Storing a string to
631          refer to the signature method mitigates complications when
632          pickling.
633      consumer_key: string Domain identifying third_party web application.
634      consumer_secret: string (optional) Secret generated during registration.
635          Required only for HMAC_SHA1 signature method.
636      rsa_key: string (optional) Private key required for RSA_SHA1 signature
637          method.
638      requestor_id: string (optional) User email adress to make requests on
639          their behalf.  This parameter should only be set when performing
640          2 legged OAuth requests.
641    """
642    if (signature_method == OAuthSignatureMethod.RSA_SHA1
643        or signature_method == 'RSA_SHA1'):
644      self.__signature_strategy = 'RSA_SHA1'
645    elif (signature_method == OAuthSignatureMethod.HMAC_SHA1
646        or signature_method == 'HMAC_SHA1'):
647      self.__signature_strategy = 'HMAC_SHA1'
648    else:
649      self.__signature_strategy = signature_method
650    self.rsa_key = rsa_key
651    self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
652    self.requestor_id = requestor_id
653
654  def __get_signature_method(self):
655    if self.__signature_strategy == 'RSA_SHA1':
656      return OAuthSignatureMethod.RSA_SHA1(self.rsa_key, None)
657    elif self.__signature_strategy == 'HMAC_SHA1':
658      return OAuthSignatureMethod.HMAC_SHA1()
659    else:
660      return self.__signature_strategy()
661
662  def __set_signature_method(self, signature_method):
663    if (signature_method == OAuthSignatureMethod.RSA_SHA1
664        or signature_method == 'RSA_SHA1'):
665      self.__signature_strategy = 'RSA_SHA1'
666    elif (signature_method == OAuthSignatureMethod.HMAC_SHA1
667        or signature_method == 'HMAC_SHA1'):
668      self.__signature_strategy = 'HMAC_SHA1'
669    else:
670      self.__signature_strategy = signature_method
671
672  _signature_method = property(__get_signature_method, __set_signature_method,
673      doc="""Returns object capable of signing the request using RSA of HMAC.
674      
675      Replaces the _signature_method member to avoid pickle errors.""")
676
677  def GetSignatureMethod(self):
678    """Gets the OAuth signature method.
679
680    Returns:
681      object of supertype <oauth.oauth.OAuthSignatureMethod>
682    """
683    return self._signature_method
684
685  def GetConsumer(self):
686    """Gets the OAuth consumer.
687
688    Returns:
689      object of type <oauth.oauth.Consumer>
690    """
691    return self._consumer
692
693
694class ClientLoginToken(atom.http_interface.GenericToken):
695  """Stores the Authorization header in auth_header and adds to requests.
696
697  This token will add it's Authorization header to an HTTP request
698  as it is made. Ths token class is simple but
699  some Token classes must calculate portions of the Authorization header
700  based on the request being made, which is why the token is responsible
701  for making requests via an http_client parameter.
702
703  Args:
704    auth_header: str The value for the Authorization header.
705    scopes: list of str or atom.url.Url specifying the beginnings of URLs
706        for which this token can be used. For example, if scopes contains
707        'http://example.com/foo', then this token can be used for a request to
708        'http://example.com/foo/bar' but it cannot be used for a request to
709        'http://example.com/baz'
710  """
711  def __init__(self, auth_header=None, scopes=None):
712    self.auth_header = auth_header
713    self.scopes = scopes or []
714
715  def __str__(self):
716    return self.auth_header
717
718  def perform_request(self, http_client, operation, url, data=None,
719                      headers=None):
720    """Sets the Authorization header and makes the HTTP request."""
721    if headers is None:
722      headers = {'Authorization':self.auth_header}
723    else:
724      headers['Authorization'] = self.auth_header
725    return http_client.request(operation, url, data=data, headers=headers)
726
727  def get_token_string(self):
728    """Removes PROGRAMMATIC_AUTH_LABEL to give just the token value."""
729    return self.auth_header[len(PROGRAMMATIC_AUTH_LABEL):]
730
731  def set_token_string(self, token_string):
732    self.auth_header = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, token_string)
733  
734  def valid_for_scope(self, url):
735    """Tells the caller if the token authorizes access to the desired URL.
736    """
737    if isinstance(url, (str, unicode)):
738      url = atom.url.parse_url(url)
739    for scope in self.scopes:
740      if scope == atom.token_store.SCOPE_ALL:
741        return True
742      if isinstance(scope, (str, unicode)):
743        scope = atom.url.parse_url(scope)
744      if scope == url:
745        return True
746      # Check the host and the path, but ignore the port and protocol.
747      elif scope.host == url.host and not scope.path:
748        return True
749      elif scope.host == url.host and scope.path and not url.path:
750        continue
751      elif scope.host == url.host and url.path.startswith(scope.path):
752        return True
753    return False
754
755
756class AuthSubToken(ClientLoginToken):
757  def get_token_string(self):
758    """Removes AUTHSUB_AUTH_LABEL to give just the token value."""
759    return self.auth_header[len(AUTHSUB_AUTH_LABEL):]
760
761  def set_token_string(self, token_string):
762    self.auth_header = '%s%s' % (AUTHSUB_AUTH_LABEL, token_string)
763
764
765class OAuthToken(atom.http_interface.GenericToken):
766  """Stores the token key, token secret and scopes for which token is valid.
767  
768  This token adds the authorization header to each request made. It
769  re-calculates authorization header for every request since the OAuth
770  signature to be added to the authorization header is dependent on the
771  request parameters.
772  
773  Attributes:
774    key: str The value for the OAuth token i.e. token key.
775    secret: str The value for the OAuth token secret.
776    scopes: list of str or atom.url.Url specifying the beginnings of URLs
777        for which this token can be used. For example, if scopes contains
778        'http://example.com/foo', then this token can be used for a request to
779        'http://example.com/foo/bar' but it cannot be used for a request to
780        'http://example.com/baz'
781    oauth_input_params: OAuthInputParams OAuth input parameters.      
782  """
783  
784  def __init__(self, key=None, secret=None, scopes=None,
785               oauth_input_params=None):
786    self.key = key
787    self.secret = secret
788    self.scopes = scopes or []
789    self.oauth_input_params = oauth_input_params
790  
791  def __str__(self):
792    return self.get_token_string()
793
794  def get_token_string(self):
795    """Returns the token string.
796    
797    The token string returned is of format
798    oauth_token=[0]&oauth_token_secret=[1], where [0] and [1] are some strings.
799    
800    Returns:
801      A token string of format oauth_token=[0]&oauth_token_secret=[1],
802      where [0] and [1] are some strings. If self.secret is absent, it just
803      returns oauth_token=[0]. If self.key is absent, it just returns
804      oauth_token_secret=[1]. If both are absent, it returns None.
805    """
806    if self.key and self.secret:
807      return urllib.urlencode({'oauth_token': self.key,
808                               'oauth_token_secret': self.secret})
809    elif self.key:
810      return 'oauth_token=%s' % self.key
811    elif self.secret:
812      return 'oauth_token_secret=%s' % self.secret
813    else:
814      return None
815
816  def set_token_string(self, token_string):
817    """Sets the token key and secret from the token string.
818    
819    Args:
820      token_string: str Token string of form
821          oauth_token=[0]&oauth_token_secret=[1]. If oauth_token is not present,
822          self.key will be None. If oauth_token_secret is not present,
823          self.secret will be None.
824    """
825    token_params = cgi.parse_qs(token_string, keep_blank_values=False)
826    if 'oauth_token' in token_params:
827      self.key = token_params['oauth_token'][0]
828    if 'oauth_token_secret' in token_params:
829      self.secret = token_params['oauth_token_secret'][0]
830    
831  def GetAuthHeader(self, http_method, http_url, realm=''):
832    """Get the authentication header.
833
834    Args:
835      http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc.
836      http_url: string or atom.url.Url HTTP URL to which request is made.
837      realm: string (default='') realm parameter to be included in the
838          authorization header.
839
840    Returns:
841      dict Header to be sent with every subsequent request after
842      authentication.
843    """
844    if isinstance(http_url, types.StringTypes):
845      http_url = atom.url.parse_url(http_url)
846    header = None
847    token = None
848    if self.key or self.secret:
849      token = oauth.OAuthToken(self.key, self.secret)
850    oauth_request = oauth.OAuthRequest.from_consumer_and_token(
851        self.oauth_input_params.GetConsumer(), token=token,
852        http_url=str(http_url), http_method=http_method,
853        parameters=http_url.params)
854    oauth_request.sign_request(self.oauth_input_params.GetSignatureMethod(),
855                               self.oauth_input_params.GetConsumer(), token)
856    header = oauth_request.to_header(realm=realm)
857    header['Authorization'] = header['Authorization'].replace('+', '%2B')
858    return header
859  
860  def perform_request(self, http_client, operation, url, data=None,
861                      headers=None):
862    """Sets the Authorization header and makes the HTTP request."""
863    if not headers:
864      headers = {}
865    if self.oauth_input_params.requestor_id:
866      url.params['xoauth_requestor_id'] = self.oauth_input_params.requestor_id
867    headers.update(self.GetAuthHeader(operation, url))
868    return http_client.request(operation, url, data=data, headers=headers)
869    
870  def valid_for_scope(self, url):
871    if isinstance(url, (str, unicode)):
872      url = atom.url.parse_url(url)
873    for scope in self.scopes:
874      if scope == atom.token_store.SCOPE_ALL:
875        return True
876      if isinstance(scope, (str, unicode)):
877        scope = atom.url.parse_url(scope)
878      if scope == url:
879        return True
880      # Check the host and the path, but ignore the port and protocol.
881      elif scope.host == url.host and not scope.path:
882        return True
883      elif scope.host == url.host and scope.path and not url.path:
884        continue
885      elif scope.host == url.host and url.path.startswith(scope.path):
886        return True
887    return False    
888    
889
890class SecureAuthSubToken(AuthSubToken):
891  """Stores the rsa private key, token, and scopes for the secure AuthSub token.
892  
893  This token adds the authorization header to each request made. It
894  re-calculates authorization header for every request since the secure AuthSub
895  signature to be added to the authorization header is dependent on the
896  request parameters.
897  
898  Attributes:
899    rsa_key: string The RSA private key in PEM format that the token will
900             use to sign requests
901    token_string: string (optional) The value for the AuthSub token.
902    scopes: list of str or atom.url.Url specifying the beginnings of URLs
903        for which this token can be used. For example, if scopes contains
904        'http://example.com/foo', then this token can be used for a request to
905        'http://example.com/foo/bar' but it cannot be used for a request to
906        'http://example.com/baz'     
907  """
908  
909  def __init__(self, rsa_key, token_string=None, scopes=None):
910    self.rsa_key = keyfactory.parsePEMKey(rsa_key)
911    self.token_string = token_string or ''
912    self.scopes = scopes or [] 
913   
914  def __str__(self):
915    return self.get_token_string()
916
917  def get_token_string(self):
918    return str(self.token_string)
919
920  def set_token_string(self, token_string):
921    self.token_string = token_string
922    
923  def GetAuthHeader(self, http_method, http_url):
924    """Generates the Authorization header.
925
926    The form of the secure AuthSub Authorization header is
927    Authorization: AuthSub token="token" sigalg="sigalg" data="data" sig="sig"
928    and  data represents a string in the form
929    data = http_method http_url timestamp nonce
930
931    Args:
932      http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc.
933      http_url: string or atom.url.Url HTTP URL to which request is made.
934      
935    Returns:
936      dict Header to be sent with every subsequent request after authentication.
937    """
938    timestamp = int(math.floor(time.time()))
939    nonce = '%lu' % random.randrange(1, 2**64)
940    data = '%s %s %d %s' % (http_method, str(http_url), timestamp, nonce)
941    sig = cryptomath.bytesToBase64(self.rsa_key.hashAndSign(data))
942    header = {'Authorization': '%s"%s" data="%s" sig="%s" sigalg="rsa-sha1"' %
943              (AUTHSUB_AUTH_LABEL, self.token_string, data, sig)}
944    return header
945  
946  def perform_request(self, http_client, operation, url, data=None, 
947                      headers=None):
948    """Sets the Authorization header and makes the HTTP request."""
949    if not headers:
950      headers = {}
951    headers.update(self.GetAuthHeader(operation, url))
952    return http_client.request(operation, url, data=data, headers=headers)