home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / pyshared / gdata / auth.py < prev    next >
Encoding:
Python Source  |  2009-07-01  |  37.0 KB  |  953 lines

  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.  
  18. import cgi
  19. import math
  20. import random
  21. import re
  22. import time
  23. import types
  24. import urllib
  25. import atom.http_interface
  26. import atom.token_store
  27. import atom.url
  28. import gdata.oauth as oauth
  29. import gdata.oauth.rsa as oauth_rsa
  30. import gdata.tlslite.utils.keyfactory as keyfactory
  31. import gdata.tlslite.utils.cryptomath as cryptomath
  32.  
  33. import gdata.gauth
  34.  
  35. __author__ = 'api.jscudder (Jeff Scudder)'
  36.  
  37.  
  38. PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
  39. AUTHSUB_AUTH_LABEL = 'AuthSub token='
  40.  
  41.  
  42. """This module provides functions and objects used with Google authentication.
  43.  
  44. Details on Google authorization mechanisms used with the Google Data APIs can
  45. be found here: 
  46. http://code.google.com/apis/gdata/auth.html
  47. http://code.google.com/apis/accounts/
  48.  
  49. The essential functions are the following.
  50. Related 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.  
  60. Related 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.  
  74. def 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.  
  99. GenerateClientLoginRequestBody = generate_client_login_request_body
  100.  
  101.  
  102. def 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.  
  121. def 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.  
  137. def 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.  
  158. def 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.  
  183. GetCaptchaChallenge = get_captcha_challenge
  184.  
  185.  
  186. def 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.  
  221. def 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.  
  268. def 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.  
  304. def 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.  
  350. def 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.  
  409. def 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.  
  426. def 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.  
  445. def 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.  
  484. def 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.  
  505. def 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.  
  526. TokenFromHttpBody = token_from_http_body
  527.  
  528.  
  529. def 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.  
  566. def 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.  
  581. class 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.  
  604. class 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.  
  694. class 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.  
  756. class 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.  
  765. class 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.  
  890. class 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)
  953.