1307 lines
48 KiB
Python
1307 lines
48 KiB
Python
|
#!/usr/bin/env python
|
||
|
#
|
||
|
# Copyright (C) 2009 Google Inc.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
|
||
|
# This module is used for version 2 of the Google Data APIs.
|
||
|
|
||
|
|
||
|
"""Provides auth related token classes and functions for Google Data APIs.
|
||
|
|
||
|
Token classes represent a user's authorization of this app to access their
|
||
|
data. Usually these are not created directly but by a GDClient object.
|
||
|
|
||
|
ClientLoginToken
|
||
|
AuthSubToken
|
||
|
SecureAuthSubToken
|
||
|
OAuthHmacToken
|
||
|
OAuthRsaToken
|
||
|
TwoLeggedOAuthHmacToken
|
||
|
TwoLeggedOAuthRsaToken
|
||
|
|
||
|
Functions which are often used in application code (as opposed to just within
|
||
|
the gdata-python-client library) are the following:
|
||
|
|
||
|
generate_auth_sub_url
|
||
|
authorize_request_token
|
||
|
|
||
|
The following are helper functions which are used to save and load auth token
|
||
|
objects in the App Engine datastore. These should only be used if you are using
|
||
|
this library within App Engine:
|
||
|
|
||
|
ae_load
|
||
|
ae_save
|
||
|
"""
|
||
|
|
||
|
|
||
|
import time
|
||
|
import random
|
||
|
import urllib
|
||
|
import atom.http_core
|
||
|
|
||
|
|
||
|
__author__ = 'j.s@google.com (Jeff Scudder)'
|
||
|
|
||
|
|
||
|
PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
|
||
|
AUTHSUB_AUTH_LABEL = 'AuthSub token='
|
||
|
|
||
|
|
||
|
# This dict provides the AuthSub and OAuth scopes for all services by service
|
||
|
# name. The service name (key) is used in ClientLogin requests.
|
||
|
AUTH_SCOPES = {
|
||
|
'cl': ( # Google Calendar API
|
||
|
'https://www.google.com/calendar/feeds/',
|
||
|
'http://www.google.com/calendar/feeds/'),
|
||
|
'gbase': ( # Google Base API
|
||
|
'http://base.google.com/base/feeds/',
|
||
|
'http://www.google.com/base/feeds/'),
|
||
|
'blogger': ( # Blogger API
|
||
|
'http://www.blogger.com/feeds/',),
|
||
|
'codesearch': ( # Google Code Search API
|
||
|
'http://www.google.com/codesearch/feeds/',),
|
||
|
'cp': ( # Contacts API
|
||
|
'https://www.google.com/m8/feeds/',
|
||
|
'http://www.google.com/m8/feeds/'),
|
||
|
'finance': ( # Google Finance API
|
||
|
'http://finance.google.com/finance/feeds/',),
|
||
|
'health': ( # Google Health API
|
||
|
'https://www.google.com/health/feeds/',),
|
||
|
'writely': ( # Documents List API
|
||
|
'https://docs.google.com/feeds/',
|
||
|
'http://docs.google.com/feeds/'),
|
||
|
'lh2': ( # Picasa Web Albums API
|
||
|
'http://picasaweb.google.com/data/',),
|
||
|
'apps': ( # Google Apps Provisioning API
|
||
|
'http://www.google.com/a/feeds/',
|
||
|
'https://www.google.com/a/feeds/',
|
||
|
'http://apps-apis.google.com/a/feeds/',
|
||
|
'https://apps-apis.google.com/a/feeds/'),
|
||
|
'weaver': ( # Health H9 Sandbox
|
||
|
'https://www.google.com/h9/feeds/',),
|
||
|
'wise': ( # Spreadsheets Data API
|
||
|
'https://spreadsheets.google.com/feeds/',
|
||
|
'http://spreadsheets.google.com/feeds/'),
|
||
|
'sitemaps': ( # Google Webmaster Tools API
|
||
|
'https://www.google.com/webmasters/tools/feeds/',),
|
||
|
'youtube': ( # YouTube API
|
||
|
'http://gdata.youtube.com/feeds/api/',
|
||
|
'http://uploads.gdata.youtube.com/feeds/api',
|
||
|
'http://gdata.youtube.com/action/GetUploadToken'),
|
||
|
'books': ( # Google Books API
|
||
|
'http://www.google.com/books/feeds/',),
|
||
|
'analytics': ( # Google Analytics API
|
||
|
'https://www.google.com/analytics/feeds/',),
|
||
|
'jotspot': ( # Google Sites API
|
||
|
'http://sites.google.com/feeds/',
|
||
|
'https://sites.google.com/feeds/'),
|
||
|
'local': ( # Google Maps Data API
|
||
|
'http://maps.google.com/maps/feeds/',),
|
||
|
'code': ( # Project Hosting Data API
|
||
|
'http://code.google.com/feeds/issues',)}
|
||
|
|
||
|
|
||
|
|
||
|
class Error(Exception):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class UnsupportedTokenType(Error):
|
||
|
"""Raised when token to or from blob is unable to convert the token."""
|
||
|
pass
|
||
|
|
||
|
|
||
|
# ClientLogin functions and classes.
|
||
|
def generate_client_login_request_body(email, password, service, source,
|
||
|
account_type='HOSTED_OR_GOOGLE', captcha_token=None,
|
||
|
captcha_response=None):
|
||
|
"""Creates the body of the autentication request
|
||
|
|
||
|
See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request
|
||
|
for more details.
|
||
|
|
||
|
Args:
|
||
|
email: str
|
||
|
password: str
|
||
|
service: str
|
||
|
source: str
|
||
|
account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid
|
||
|
values are 'GOOGLE' and 'HOSTED'
|
||
|
captcha_token: str (optional)
|
||
|
captcha_response: str (optional)
|
||
|
|
||
|
Returns:
|
||
|
The HTTP body to send in a request for a client login token.
|
||
|
"""
|
||
|
# Create a POST body containing the user's credentials.
|
||
|
request_fields = {'Email': email,
|
||
|
'Passwd': password,
|
||
|
'accountType': account_type,
|
||
|
'service': service,
|
||
|
'source': source}
|
||
|
if captcha_token and captcha_response:
|
||
|
# Send the captcha token and response as part of the POST body if the
|
||
|
# user is responding to a captch challenge.
|
||
|
request_fields['logintoken'] = captcha_token
|
||
|
request_fields['logincaptcha'] = captcha_response
|
||
|
return urllib.urlencode(request_fields)
|
||
|
|
||
|
|
||
|
GenerateClientLoginRequestBody = generate_client_login_request_body
|
||
|
|
||
|
|
||
|
def get_client_login_token_string(http_body):
|
||
|
"""Returns the token value for a ClientLoginToken.
|
||
|
|
||
|
Reads the token from the server's response to a Client Login request and
|
||
|
creates the token value string to use in requests.
|
||
|
|
||
|
Args:
|
||
|
http_body: str The body of the server's HTTP response to a Client Login
|
||
|
request
|
||
|
|
||
|
Returns:
|
||
|
The token value string for a ClientLoginToken.
|
||
|
"""
|
||
|
for response_line in http_body.splitlines():
|
||
|
if response_line.startswith('Auth='):
|
||
|
# Strip off the leading Auth= and return the Authorization value.
|
||
|
return response_line[5:]
|
||
|
return None
|
||
|
|
||
|
|
||
|
GetClientLoginTokenString = get_client_login_token_string
|
||
|
|
||
|
|
||
|
def get_captcha_challenge(http_body,
|
||
|
captcha_base_url='http://www.google.com/accounts/'):
|
||
|
"""Returns the URL and token for a CAPTCHA challenge issued by the server.
|
||
|
|
||
|
Args:
|
||
|
http_body: str The body of the HTTP response from the server which
|
||
|
contains the CAPTCHA challenge.
|
||
|
captcha_base_url: str This function returns a full URL for viewing the
|
||
|
challenge image which is built from the server's response. This
|
||
|
base_url is used as the beginning of the URL because the server
|
||
|
only provides the end of the URL. For example the server provides
|
||
|
'Captcha?ctoken=Hi...N' and the URL for the image is
|
||
|
'http://www.google.com/accounts/Captcha?ctoken=Hi...N'
|
||
|
|
||
|
Returns:
|
||
|
A dictionary containing the information needed to repond to the CAPTCHA
|
||
|
challenge, the image URL and the ID token of the challenge. The
|
||
|
dictionary is in the form:
|
||
|
{'token': string identifying the CAPTCHA image,
|
||
|
'url': string containing the URL of the image}
|
||
|
Returns None if there was no CAPTCHA challenge in the response.
|
||
|
"""
|
||
|
contains_captcha_challenge = False
|
||
|
captcha_parameters = {}
|
||
|
for response_line in http_body.splitlines():
|
||
|
if response_line.startswith('Error=CaptchaRequired'):
|
||
|
contains_captcha_challenge = True
|
||
|
elif response_line.startswith('CaptchaToken='):
|
||
|
# Strip off the leading CaptchaToken=
|
||
|
captcha_parameters['token'] = response_line[13:]
|
||
|
elif response_line.startswith('CaptchaUrl='):
|
||
|
captcha_parameters['url'] = '%s%s' % (captcha_base_url,
|
||
|
response_line[11:])
|
||
|
if contains_captcha_challenge:
|
||
|
return captcha_parameters
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
GetCaptchaChallenge = get_captcha_challenge
|
||
|
|
||
|
|
||
|
class ClientLoginToken(object):
|
||
|
|
||
|
def __init__(self, token_string):
|
||
|
self.token_string = token_string
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
http_request.headers['Authorization'] = '%s%s' % (PROGRAMMATIC_AUTH_LABEL,
|
||
|
self.token_string)
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
# AuthSub functions and classes.
|
||
|
def _to_uri(str_or_uri):
|
||
|
if isinstance(str_or_uri, (str, unicode)):
|
||
|
return atom.http_core.Uri.parse_uri(str_or_uri)
|
||
|
return str_or_uri
|
||
|
|
||
|
|
||
|
def generate_auth_sub_url(next, scopes, secure=False, session=True,
|
||
|
request_url=atom.http_core.parse_uri(
|
||
|
'https://www.google.com/accounts/AuthSubRequest'),
|
||
|
domain='default', scopes_param_prefix='auth_sub_scopes'):
|
||
|
"""Constructs a URI for requesting a multiscope AuthSub token.
|
||
|
|
||
|
The generated token will contain a URL parameter to pass along the
|
||
|
requested scopes to the next URL. When the Google Accounts page
|
||
|
redirects the broswser to the 'next' URL, it appends the single use
|
||
|
AuthSub token value to the URL as a URL parameter with the key 'token'.
|
||
|
However, the information about which scopes were requested is not
|
||
|
included by Google Accounts. This method adds the scopes to the next
|
||
|
URL before making the request so that the redirect will be sent to
|
||
|
a page, and both the token value and the list of scopes for which the token
|
||
|
was requested.
|
||
|
|
||
|
Args:
|
||
|
next: atom.http_core.Uri or string The URL user will be sent to after
|
||
|
authorizing this web application to access their data.
|
||
|
scopes: list containint strings or atom.http_core.Uri objects. The URLs
|
||
|
of the services to be accessed. Could also be a single string
|
||
|
or single atom.http_core.Uri for requesting just one scope.
|
||
|
secure: boolean (optional) Determines whether or not the issued token
|
||
|
is a secure token.
|
||
|
session: boolean (optional) Determines whether or not the issued token
|
||
|
can be upgraded to a session token.
|
||
|
request_url: atom.http_core.Uri or str The beginning of the request URL.
|
||
|
This is normally
|
||
|
'http://www.google.com/accounts/AuthSubRequest' or
|
||
|
'/accounts/AuthSubRequest'
|
||
|
domain: The domain which the account is part of. This is used for Google
|
||
|
Apps accounts, the default value is 'default' which means that
|
||
|
the requested account is a Google Account (@gmail.com for
|
||
|
example)
|
||
|
scopes_param_prefix: str (optional) The requested scopes are added as a
|
||
|
URL parameter to the next URL so that the page at
|
||
|
the 'next' URL can extract the token value and the
|
||
|
valid scopes from the URL. The key for the URL
|
||
|
parameter defaults to 'auth_sub_scopes'
|
||
|
|
||
|
Returns:
|
||
|
An atom.http_core.Uri which the user's browser should be directed to in
|
||
|
order to authorize this application to access their information.
|
||
|
"""
|
||
|
if isinstance(next, (str, unicode)):
|
||
|
next = atom.http_core.Uri.parse_uri(next)
|
||
|
# If the user passed in a string instead of a list for scopes, convert to
|
||
|
# a single item tuple.
|
||
|
if isinstance(scopes, (str, unicode, atom.http_core.Uri)):
|
||
|
scopes = (scopes,)
|
||
|
scopes_string = ' '.join([str(scope) for scope in scopes])
|
||
|
next.query[scopes_param_prefix] = scopes_string
|
||
|
|
||
|
if isinstance(request_url, (str, unicode)):
|
||
|
request_url = atom.http_core.Uri.parse_uri(request_url)
|
||
|
request_url.query['next'] = str(next)
|
||
|
request_url.query['scope'] = scopes_string
|
||
|
if session:
|
||
|
request_url.query['session'] = '1'
|
||
|
else:
|
||
|
request_url.query['session'] = '0'
|
||
|
if secure:
|
||
|
request_url.query['secure'] = '1'
|
||
|
else:
|
||
|
request_url.query['secure'] = '0'
|
||
|
request_url.query['hd'] = domain
|
||
|
return request_url
|
||
|
|
||
|
|
||
|
def auth_sub_string_from_url(url, scopes_param_prefix='auth_sub_scopes'):
|
||
|
"""Finds the token string (and scopes) after the browser is redirected.
|
||
|
|
||
|
After the Google Accounts AuthSub pages redirect the user's broswer back to
|
||
|
the web application (using the 'next' URL from the request) the web app must
|
||
|
extract the token from the current page's URL. The token is provided as a
|
||
|
URL parameter named 'token' and if generate_auth_sub_url was used to create
|
||
|
the request, the token's valid scopes are included in a URL parameter whose
|
||
|
name is specified in scopes_param_prefix.
|
||
|
|
||
|
Args:
|
||
|
url: atom.url.Url or str representing the current URL. The token value
|
||
|
and valid scopes should be included as URL parameters.
|
||
|
scopes_param_prefix: str (optional) The URL parameter key which maps to
|
||
|
the list of valid scopes for the token.
|
||
|
|
||
|
Returns:
|
||
|
A tuple containing the token value as a string, and a tuple of scopes
|
||
|
(as atom.http_core.Uri objects) which are URL prefixes under which this
|
||
|
token grants permission to read and write user data.
|
||
|
(token_string, (scope_uri, scope_uri, scope_uri, ...))
|
||
|
If no scopes were included in the URL, the second value in the tuple is
|
||
|
None. If there was no token param in the url, the tuple returned is
|
||
|
(None, None)
|
||
|
"""
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.http_core.Uri.parse_uri(url)
|
||
|
if 'token' not in url.query:
|
||
|
return (None, None)
|
||
|
token = url.query['token']
|
||
|
# TODO: decide whether no scopes should be None or ().
|
||
|
scopes = None # Default to None for no scopes.
|
||
|
if scopes_param_prefix in url.query:
|
||
|
scopes = tuple(url.query[scopes_param_prefix].split(' '))
|
||
|
return (token, scopes)
|
||
|
|
||
|
|
||
|
AuthSubStringFromUrl = auth_sub_string_from_url
|
||
|
|
||
|
|
||
|
def auth_sub_string_from_body(http_body):
|
||
|
"""Extracts the AuthSub token from an HTTP body string.
|
||
|
|
||
|
Used to find the new session token after making a request to upgrade a
|
||
|
single use AuthSub token.
|
||
|
|
||
|
Args:
|
||
|
http_body: str The repsonse from the server which contains the AuthSub
|
||
|
key. For example, this function would find the new session token
|
||
|
from the server's response to an upgrade token request.
|
||
|
|
||
|
Returns:
|
||
|
The raw token value string to use in an AuthSubToken object.
|
||
|
"""
|
||
|
for response_line in http_body.splitlines():
|
||
|
if response_line.startswith('Token='):
|
||
|
# Strip off Token= and return the token value string.
|
||
|
return response_line[6:]
|
||
|
return None
|
||
|
|
||
|
|
||
|
class AuthSubToken(object):
|
||
|
|
||
|
def __init__(self, token_string, scopes=None):
|
||
|
self.token_string = token_string
|
||
|
self.scopes = scopes or []
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets Authorization header, allows app to act on the user's behalf."""
|
||
|
http_request.headers['Authorization'] = '%s%s' % (AUTHSUB_AUTH_LABEL,
|
||
|
self.token_string)
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
def from_url(str_or_uri):
|
||
|
"""Creates a new AuthSubToken using information in the URL.
|
||
|
|
||
|
Uses auth_sub_string_from_url.
|
||
|
|
||
|
Args:
|
||
|
str_or_uri: The current page's URL (as a str or atom.http_core.Uri)
|
||
|
which should contain a token query parameter since the
|
||
|
Google auth server redirected the user's browser to this
|
||
|
URL.
|
||
|
"""
|
||
|
token_and_scopes = auth_sub_string_from_url(str_or_uri)
|
||
|
return AuthSubToken(token_and_scopes[0], token_and_scopes[1])
|
||
|
|
||
|
from_url = staticmethod(from_url)
|
||
|
FromUrl = from_url
|
||
|
|
||
|
def _upgrade_token(self, http_body):
|
||
|
"""Replaces the token value with a session token from the auth server.
|
||
|
|
||
|
Uses the response of a token upgrade request to modify this token. Uses
|
||
|
auth_sub_string_from_body.
|
||
|
"""
|
||
|
self.token_string = auth_sub_string_from_body(http_body)
|
||
|
|
||
|
|
||
|
# Functions and classes for Secure-mode AuthSub
|
||
|
def build_auth_sub_data(http_request, timestamp, nonce):
|
||
|
"""Creates the data string which must be RSA-signed in secure requests.
|
||
|
|
||
|
For more details see the documenation on secure AuthSub requests:
|
||
|
http://code.google.com/apis/accounts/docs/AuthSub.html#signingrequests
|
||
|
|
||
|
Args:
|
||
|
http_request: The request being made to the server. The Request's URL
|
||
|
must be complete before this signature is calculated as any changes
|
||
|
to the URL will invalidate the signature.
|
||
|
nonce: str Random 64-bit, unsigned number encoded as an ASCII string in
|
||
|
decimal format. The nonce/timestamp pair should always be unique to
|
||
|
prevent replay attacks.
|
||
|
timestamp: Integer representing the time the request is sent. The
|
||
|
timestamp should be expressed in number of seconds after January 1,
|
||
|
1970 00:00:00 GMT.
|
||
|
"""
|
||
|
return '%s %s %s %s' % (http_request.method, str(http_request.uri),
|
||
|
str(timestamp), nonce)
|
||
|
|
||
|
|
||
|
def generate_signature(data, rsa_key):
|
||
|
"""Signs the data string for a secure AuthSub request."""
|
||
|
import base64
|
||
|
try:
|
||
|
from tlslite.utils import keyfactory
|
||
|
except ImportError:
|
||
|
from gdata.tlslite.utils import keyfactory
|
||
|
private_key = keyfactory.parsePrivateKey(rsa_key)
|
||
|
signed = private_key.hashAndSign(data)
|
||
|
# Python2.3 and lower does not have the base64.b64encode function.
|
||
|
if hasattr(base64, 'b64encode'):
|
||
|
return base64.b64encode(signed)
|
||
|
else:
|
||
|
return base64.encodestring(signed).replace('\n', '')
|
||
|
|
||
|
|
||
|
class SecureAuthSubToken(AuthSubToken):
|
||
|
|
||
|
def __init__(self, token_string, rsa_private_key, scopes=None):
|
||
|
self.token_string = token_string
|
||
|
self.scopes = scopes or []
|
||
|
self.rsa_private_key = rsa_private_key
|
||
|
|
||
|
def from_url(str_or_uri, rsa_private_key):
|
||
|
"""Creates a new SecureAuthSubToken using information in the URL.
|
||
|
|
||
|
Uses auth_sub_string_from_url.
|
||
|
|
||
|
Args:
|
||
|
str_or_uri: The current page's URL (as a str or atom.http_core.Uri)
|
||
|
which should contain a token query parameter since the Google auth
|
||
|
server redirected the user's browser to this URL.
|
||
|
rsa_private_key: str the private RSA key cert used to sign all requests
|
||
|
made with this token.
|
||
|
"""
|
||
|
token_and_scopes = auth_sub_string_from_url(str_or_uri)
|
||
|
return SecureAuthSubToken(token_and_scopes[0], rsa_private_key,
|
||
|
token_and_scopes[1])
|
||
|
|
||
|
from_url = staticmethod(from_url)
|
||
|
FromUrl = from_url
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets the Authorization header and includes a digital signature.
|
||
|
|
||
|
Calculates a digital signature using the private RSA key, a timestamp
|
||
|
(uses now at the time this method is called) and a random nonce.
|
||
|
|
||
|
Args:
|
||
|
http_request: The atom.http_core.HttpRequest which contains all of the
|
||
|
information needed to send a request to the remote server. The
|
||
|
URL and the method of the request must be already set and cannot be
|
||
|
changed after this token signs the request, or the signature will
|
||
|
not be valid.
|
||
|
"""
|
||
|
timestamp = str(int(time.time()))
|
||
|
nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
|
||
|
data = build_auth_sub_data(http_request, timestamp, nonce)
|
||
|
signature = generate_signature(data, self.rsa_private_key)
|
||
|
http_request.headers['Authorization'] = (
|
||
|
'%s%s sigalg="rsa-sha1" data="%s" sig="%s"' % (AUTHSUB_AUTH_LABEL,
|
||
|
self.token_string, data, signature))
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
# OAuth functions and classes.
|
||
|
RSA_SHA1 = 'RSA-SHA1'
|
||
|
HMAC_SHA1 = 'HMAC-SHA1'
|
||
|
|
||
|
|
||
|
def build_oauth_base_string(http_request, consumer_key, nonce, signaure_type,
|
||
|
timestamp, version, next='oob', token=None,
|
||
|
verifier=None):
|
||
|
"""Generates the base string to be signed in the OAuth request.
|
||
|
|
||
|
Args:
|
||
|
http_request: The request being made to the server. The Request's URL
|
||
|
must be complete before this signature is calculated as any changes
|
||
|
to the URL will invalidate the signature.
|
||
|
consumer_key: Domain identifying the third-party web application. This is
|
||
|
the domain used when registering the application with Google. It
|
||
|
identifies who is making the request on behalf of the user.
|
||
|
nonce: Random 64-bit, unsigned number encoded as an ASCII string in decimal
|
||
|
format. The nonce/timestamp pair should always be unique to prevent
|
||
|
replay attacks.
|
||
|
signaure_type: either RSA_SHA1 or HMAC_SHA1
|
||
|
timestamp: Integer representing the time the request is sent. The
|
||
|
timestamp should be expressed in number of seconds after January 1,
|
||
|
1970 00:00:00 GMT.
|
||
|
version: The OAuth version used by the requesting web application. This
|
||
|
value must be '1.0' or '1.0a'. If not provided, Google assumes version
|
||
|
1.0 is in use.
|
||
|
next: The URL the user should be redirected to after granting access
|
||
|
to a Google service(s). It can include url-encoded query parameters.
|
||
|
The default value is 'oob'. (This is the oauth_callback.)
|
||
|
token: The string for the OAuth request token or OAuth access token.
|
||
|
verifier: str Sent as the oauth_verifier and required when upgrading a
|
||
|
request token to an access token.
|
||
|
"""
|
||
|
# First we must build the canonical base string for the request.
|
||
|
params = http_request.uri.query.copy()
|
||
|
params['oauth_consumer_key'] = consumer_key
|
||
|
params['oauth_nonce'] = nonce
|
||
|
params['oauth_signature_method'] = signaure_type
|
||
|
params['oauth_timestamp'] = str(timestamp)
|
||
|
if next is not None:
|
||
|
params['oauth_callback'] = str(next)
|
||
|
if token is not None:
|
||
|
params['oauth_token'] = token
|
||
|
if version is not None:
|
||
|
params['oauth_version'] = version
|
||
|
if verifier is not None:
|
||
|
params['oauth_verifier'] = verifier
|
||
|
# We need to get the key value pairs in lexigraphically sorted order.
|
||
|
sorted_keys = None
|
||
|
try:
|
||
|
sorted_keys = sorted(params.keys())
|
||
|
# The sorted function is not available in Python2.3 and lower
|
||
|
except NameError:
|
||
|
sorted_keys = params.keys()
|
||
|
sorted_keys.sort()
|
||
|
pairs = []
|
||
|
for key in sorted_keys:
|
||
|
pairs.append('%s=%s' % (urllib.quote(key, safe='~'),
|
||
|
urllib.quote(params[key], safe='~')))
|
||
|
# We want to escape /'s too, so use safe='~'
|
||
|
all_parameters = urllib.quote('&'.join(pairs), safe='~')
|
||
|
normailzed_host = http_request.uri.host.lower()
|
||
|
normalized_scheme = (http_request.uri.scheme or 'http').lower()
|
||
|
non_default_port = None
|
||
|
if (http_request.uri.port is not None
|
||
|
and ((normalized_scheme == 'https' and http_request.uri.port != 443)
|
||
|
or (normalized_scheme == 'http' and http_request.uri.port != 80))):
|
||
|
non_default_port = http_request.uri.port
|
||
|
path = http_request.uri.path or '/'
|
||
|
request_path = None
|
||
|
if not path.startswith('/'):
|
||
|
path = '/%s' % path
|
||
|
if non_default_port is not None:
|
||
|
# Set the only safe char in url encoding to ~ since we want to escape /
|
||
|
# as well.
|
||
|
request_path = urllib.quote('%s://%s:%s%s' % (
|
||
|
normalized_scheme, normailzed_host, non_default_port, path), safe='~')
|
||
|
else:
|
||
|
# Set the only safe char in url encoding to ~ since we want to escape /
|
||
|
# as well.
|
||
|
request_path = urllib.quote('%s://%s%s' % (
|
||
|
normalized_scheme, normailzed_host, path), safe='~')
|
||
|
# TODO: ensure that token escaping logic is correct, not sure if the token
|
||
|
# value should be double escaped instead of single.
|
||
|
base_string = '&'.join((http_request.method.upper(), request_path,
|
||
|
all_parameters))
|
||
|
# Now we have the base string, we can calculate the oauth_signature.
|
||
|
return base_string
|
||
|
|
||
|
|
||
|
def generate_hmac_signature(http_request, consumer_key, consumer_secret,
|
||
|
timestamp, nonce, version, next='oob',
|
||
|
token=None, token_secret=None, verifier=None):
|
||
|
import hmac
|
||
|
import base64
|
||
|
base_string = build_oauth_base_string(
|
||
|
http_request, consumer_key, nonce, HMAC_SHA1, timestamp, version,
|
||
|
next, token, verifier=verifier)
|
||
|
hash_key = None
|
||
|
hashed = None
|
||
|
if token_secret is not None:
|
||
|
hash_key = '%s&%s' % (urllib.quote(consumer_secret, safe='~'),
|
||
|
urllib.quote(token_secret, safe='~'))
|
||
|
else:
|
||
|
hash_key = '%s&' % urllib.quote(consumer_secret, safe='~')
|
||
|
try:
|
||
|
import hashlib
|
||
|
hashed = hmac.new(hash_key, base_string, hashlib.sha1)
|
||
|
except ImportError:
|
||
|
import sha
|
||
|
hashed = hmac.new(hash_key, base_string, sha)
|
||
|
# Python2.3 does not have base64.b64encode.
|
||
|
if hasattr(base64, 'b64encode'):
|
||
|
return base64.b64encode(hashed.digest())
|
||
|
else:
|
||
|
return base64.encodestring(hashed.digest()).replace('\n', '')
|
||
|
|
||
|
|
||
|
def generate_rsa_signature(http_request, consumer_key, rsa_key,
|
||
|
timestamp, nonce, version, next='oob',
|
||
|
token=None, token_secret=None, verifier=None):
|
||
|
import base64
|
||
|
try:
|
||
|
from tlslite.utils import keyfactory
|
||
|
except ImportError:
|
||
|
from gdata.tlslite.utils import keyfactory
|
||
|
base_string = build_oauth_base_string(
|
||
|
http_request, consumer_key, nonce, RSA_SHA1, timestamp, version,
|
||
|
next, token, verifier=verifier)
|
||
|
private_key = keyfactory.parsePrivateKey(rsa_key)
|
||
|
# Sign using the key
|
||
|
signed = private_key.hashAndSign(base_string)
|
||
|
# Python2.3 does not have base64.b64encode.
|
||
|
if hasattr(base64, 'b64encode'):
|
||
|
return base64.b64encode(signed)
|
||
|
else:
|
||
|
return base64.encodestring(signed).replace('\n', '')
|
||
|
|
||
|
|
||
|
def generate_auth_header(consumer_key, timestamp, nonce, signature_type,
|
||
|
signature, version='1.0', next=None, token=None,
|
||
|
verifier=None):
|
||
|
"""Builds the Authorization header to be sent in the request.
|
||
|
|
||
|
Args:
|
||
|
consumer_key: Identifies the application making the request (str).
|
||
|
timestamp:
|
||
|
nonce:
|
||
|
signature_type: One of either HMAC_SHA1 or RSA_SHA1
|
||
|
signature: The HMAC or RSA signature for the request as a base64
|
||
|
encoded string.
|
||
|
version: The version of the OAuth protocol that this request is using.
|
||
|
Default is '1.0'
|
||
|
next: The URL of the page that the user's browser should be sent to
|
||
|
after they authorize the token. (Optional)
|
||
|
token: str The OAuth token value to be used in the oauth_token parameter
|
||
|
of the header.
|
||
|
verifier: str The OAuth verifier which must be included when you are
|
||
|
upgrading a request token to an access token.
|
||
|
"""
|
||
|
params = {
|
||
|
'oauth_consumer_key': consumer_key,
|
||
|
'oauth_version': version,
|
||
|
'oauth_nonce': nonce,
|
||
|
'oauth_timestamp': str(timestamp),
|
||
|
'oauth_signature_method': signature_type,
|
||
|
'oauth_signature': signature}
|
||
|
if next is not None:
|
||
|
params['oauth_callback'] = str(next)
|
||
|
if token is not None:
|
||
|
params['oauth_token'] = token
|
||
|
if verifier is not None:
|
||
|
params['oauth_verifier'] = verifier
|
||
|
pairs = [
|
||
|
'%s="%s"' % (
|
||
|
k, urllib.quote(v, safe='~')) for k, v in params.iteritems()]
|
||
|
return 'OAuth %s' % (', '.join(pairs))
|
||
|
|
||
|
|
||
|
REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
|
||
|
ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
|
||
|
|
||
|
|
||
|
def generate_request_for_request_token(
|
||
|
consumer_key, signature_type, scopes, rsa_key=None, consumer_secret=None,
|
||
|
auth_server_url=REQUEST_TOKEN_URL, next='oob', version='1.0'):
|
||
|
"""Creates request to be sent to auth server to get an OAuth request token.
|
||
|
|
||
|
Args:
|
||
|
consumer_key:
|
||
|
signature_type: either RSA_SHA1 or HMAC_SHA1. The rsa_key must be
|
||
|
provided if the signature type is RSA but if the signature method
|
||
|
is HMAC, the consumer_secret must be used.
|
||
|
scopes: List of URL prefixes for the data which we want to access. For
|
||
|
example, to request access to the user's Blogger and Google Calendar
|
||
|
data, we would request
|
||
|
['http://www.blogger.com/feeds/',
|
||
|
'https://www.google.com/calendar/feeds/',
|
||
|
'http://www.google.com/calendar/feeds/']
|
||
|
rsa_key: Only used if the signature method is RSA_SHA1.
|
||
|
consumer_secret: Only used if the signature method is HMAC_SHA1.
|
||
|
auth_server_url: The URL to which the token request should be directed.
|
||
|
Defaults to 'https://www.google.com/accounts/OAuthGetRequestToken'.
|
||
|
next: The URL of the page that the user's browser should be sent to
|
||
|
after they authorize the token. (Optional)
|
||
|
version: The OAuth version used by the requesting web application.
|
||
|
Defaults to '1.0a'
|
||
|
|
||
|
Returns:
|
||
|
An atom.http_core.HttpRequest object with the URL, Authorization header
|
||
|
and body filled in.
|
||
|
"""
|
||
|
request = atom.http_core.HttpRequest(auth_server_url, 'POST')
|
||
|
# Add the requested auth scopes to the Auth request URL.
|
||
|
if scopes:
|
||
|
request.uri.query['scope'] = ' '.join(scopes)
|
||
|
|
||
|
timestamp = str(int(time.time()))
|
||
|
nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
|
||
|
signature = None
|
||
|
if signature_type == HMAC_SHA1:
|
||
|
signature = generate_hmac_signature(
|
||
|
request, consumer_key, consumer_secret, timestamp, nonce, version,
|
||
|
next=next)
|
||
|
elif signature_type == RSA_SHA1:
|
||
|
signature = generate_rsa_signature(
|
||
|
request, consumer_key, rsa_key, timestamp, nonce, version, next=next)
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
request.headers['Authorization'] = generate_auth_header(
|
||
|
consumer_key, timestamp, nonce, signature_type, signature, version,
|
||
|
next)
|
||
|
request.headers['Content-Length'] = '0'
|
||
|
return request
|
||
|
|
||
|
|
||
|
def generate_request_for_access_token(
|
||
|
request_token, auth_server_url=ACCESS_TOKEN_URL):
|
||
|
"""Creates a request to ask the OAuth server for an access token.
|
||
|
|
||
|
Requires a request token which the user has authorized. See the
|
||
|
documentation on OAuth with Google Data for more details:
|
||
|
http://code.google.com/apis/accounts/docs/OAuth.html#AccessToken
|
||
|
|
||
|
Args:
|
||
|
request_token: An OAuthHmacToken or OAuthRsaToken which the user has
|
||
|
approved using their browser.
|
||
|
auth_server_url: (optional) The URL at which the OAuth access token is
|
||
|
requested. Defaults to
|
||
|
https://www.google.com/accounts/OAuthGetAccessToken
|
||
|
|
||
|
Returns:
|
||
|
A new HttpRequest object which can be sent to the OAuth server to
|
||
|
request an OAuth Access Token.
|
||
|
"""
|
||
|
http_request = atom.http_core.HttpRequest(auth_server_url, 'POST')
|
||
|
http_request.headers['Content-Length'] = '0'
|
||
|
return request_token.modify_request(http_request)
|
||
|
|
||
|
|
||
|
def oauth_token_info_from_body(http_body):
|
||
|
"""Exracts an OAuth request token from the server's response.
|
||
|
|
||
|
Returns:
|
||
|
A tuple of strings containing the OAuth token and token secret. If
|
||
|
neither of these are present in the body, returns (None, None)
|
||
|
"""
|
||
|
token = None
|
||
|
token_secret = None
|
||
|
for pair in http_body.split('&'):
|
||
|
if pair.startswith('oauth_token='):
|
||
|
token = urllib.unquote(pair[len('oauth_token='):])
|
||
|
if pair.startswith('oauth_token_secret='):
|
||
|
token_secret = urllib.unquote(pair[len('oauth_token_secret='):])
|
||
|
return (token, token_secret)
|
||
|
|
||
|
|
||
|
def hmac_token_from_body(http_body, consumer_key, consumer_secret,
|
||
|
auth_state):
|
||
|
token_value, token_secret = oauth_token_info_from_body(http_body)
|
||
|
token = OAuthHmacToken(consumer_key, consumer_secret, token_value,
|
||
|
token_secret, auth_state)
|
||
|
return token
|
||
|
|
||
|
|
||
|
def rsa_token_from_body(http_body, consumer_key, rsa_private_key,
|
||
|
auth_state):
|
||
|
token_value, token_secret = oauth_token_info_from_body(http_body)
|
||
|
token = OAuthRsaToken(consumer_key, rsa_private_key, token_value,
|
||
|
token_secret, auth_state)
|
||
|
return token
|
||
|
|
||
|
|
||
|
DEFAULT_DOMAIN = 'default'
|
||
|
OAUTH_AUTHORIZE_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
|
||
|
|
||
|
|
||
|
def generate_oauth_authorization_url(
|
||
|
token, next=None, hd=DEFAULT_DOMAIN, hl=None, btmpl=None,
|
||
|
auth_server=OAUTH_AUTHORIZE_URL):
|
||
|
"""Creates a URL for the page where the request token can be authorized.
|
||
|
|
||
|
Args:
|
||
|
token: str The request token from the OAuth server.
|
||
|
next: str (optional) URL the user should be redirected to after granting
|
||
|
access to a Google service(s). It can include url-encoded query
|
||
|
parameters.
|
||
|
hd: str (optional) Identifies a particular hosted domain account to be
|
||
|
accessed (for example, 'mycollege.edu'). Uses 'default' to specify a
|
||
|
regular Google account ('username@gmail.com').
|
||
|
hl: str (optional) An ISO 639 country code identifying what language the
|
||
|
approval page should be translated in (for example, 'hl=en' for
|
||
|
English). The default is the user's selected language.
|
||
|
btmpl: str (optional) Forces a mobile version of the approval page. The
|
||
|
only accepted value is 'mobile'.
|
||
|
auth_server: str (optional) The start of the token authorization web
|
||
|
page. Defaults to
|
||
|
'https://www.google.com/accounts/OAuthAuthorizeToken'
|
||
|
|
||
|
Returns:
|
||
|
An atom.http_core.Uri pointing to the token authorization page where the
|
||
|
user may allow or deny this app to access their Google data.
|
||
|
"""
|
||
|
uri = atom.http_core.Uri.parse_uri(auth_server)
|
||
|
uri.query['oauth_token'] = token
|
||
|
uri.query['hd'] = hd
|
||
|
if next is not None:
|
||
|
uri.query['oauth_callback'] = str(next)
|
||
|
if hl is not None:
|
||
|
uri.query['hl'] = hl
|
||
|
if btmpl is not None:
|
||
|
uri.query['btmpl'] = btmpl
|
||
|
return uri
|
||
|
|
||
|
|
||
|
def oauth_token_info_from_url(url):
|
||
|
"""Exracts an OAuth access token from the redirected page's URL.
|
||
|
|
||
|
Returns:
|
||
|
A tuple of strings containing the OAuth token and the OAuth verifier which
|
||
|
need to sent when upgrading a request token to an access token.
|
||
|
"""
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.http_core.Uri.parse_uri(url)
|
||
|
token = None
|
||
|
verifier = None
|
||
|
if 'oauth_token' in url.query:
|
||
|
token = urllib.unquote(url.query['oauth_token'])
|
||
|
if 'oauth_verifier' in url.query:
|
||
|
verifier = urllib.unquote(url.query['oauth_verifier'])
|
||
|
return (token, verifier)
|
||
|
|
||
|
|
||
|
def authorize_request_token(request_token, url):
|
||
|
"""Adds information to request token to allow it to become an access token.
|
||
|
|
||
|
Modifies the request_token object passed in by setting and unsetting the
|
||
|
necessary fields to allow this token to form a valid upgrade request.
|
||
|
|
||
|
Args:
|
||
|
request_token: The OAuth request token which has been authorized by the
|
||
|
user. In order for this token to be upgraded to an access token,
|
||
|
certain fields must be extracted from the URL and added to the token
|
||
|
so that they can be passed in an upgrade-token request.
|
||
|
url: The URL of the current page which the user's browser was redirected
|
||
|
to after they authorized access for the app. This function extracts
|
||
|
information from the URL which is needed to upgraded the token from
|
||
|
a request token to an access token.
|
||
|
|
||
|
Returns:
|
||
|
The same token object which was passed in.
|
||
|
"""
|
||
|
token, verifier = oauth_token_info_from_url(url)
|
||
|
request_token.token = token
|
||
|
request_token.verifier = verifier
|
||
|
request_token.auth_state = AUTHORIZED_REQUEST_TOKEN
|
||
|
return request_token
|
||
|
|
||
|
|
||
|
AuthorizeRequestToken = authorize_request_token
|
||
|
|
||
|
|
||
|
def upgrade_to_access_token(request_token, server_response_body):
|
||
|
"""Extracts access token information from response to an upgrade request.
|
||
|
|
||
|
Once the server has responded with the new token info for the OAuth
|
||
|
access token, this method modifies the request_token to set and unset
|
||
|
necessary fields to create valid OAuth authorization headers for requests.
|
||
|
|
||
|
Args:
|
||
|
request_token: An OAuth token which this function modifies to allow it
|
||
|
to be used as an access token.
|
||
|
server_response_body: str The server's response to an OAuthAuthorizeToken
|
||
|
request. This should contain the new token and token_secret which
|
||
|
are used to generate the signature and parameters of the Authorization
|
||
|
header in subsequent requests to Google Data APIs.
|
||
|
|
||
|
Returns:
|
||
|
The same token object which was passed in.
|
||
|
"""
|
||
|
token, token_secret = oauth_token_info_from_body(server_response_body)
|
||
|
request_token.token = token
|
||
|
request_token.token_secret = token_secret
|
||
|
request_token.auth_state = ACCESS_TOKEN
|
||
|
request_token.next = None
|
||
|
request_token.verifier = None
|
||
|
return request_token
|
||
|
|
||
|
|
||
|
UpgradeToAccessToken = upgrade_to_access_token
|
||
|
|
||
|
|
||
|
REQUEST_TOKEN = 1
|
||
|
AUTHORIZED_REQUEST_TOKEN = 2
|
||
|
ACCESS_TOKEN = 3
|
||
|
|
||
|
|
||
|
class OAuthHmacToken(object):
|
||
|
SIGNATURE_METHOD = HMAC_SHA1
|
||
|
|
||
|
def __init__(self, consumer_key, consumer_secret, token, token_secret,
|
||
|
auth_state, next=None, verifier=None):
|
||
|
self.consumer_key = consumer_key
|
||
|
self.consumer_secret = consumer_secret
|
||
|
self.token = token
|
||
|
self.token_secret = token_secret
|
||
|
self.auth_state = auth_state
|
||
|
self.next = next
|
||
|
self.verifier = verifier # Used to convert request token to access token.
|
||
|
|
||
|
def generate_authorization_url(
|
||
|
self, google_apps_domain=DEFAULT_DOMAIN, language=None, btmpl=None,
|
||
|
auth_server=OAUTH_AUTHORIZE_URL):
|
||
|
"""Creates the URL at which the user can authorize this app to access.
|
||
|
|
||
|
Args:
|
||
|
google_apps_domain: str (optional) If the user should be signing in
|
||
|
using an account under a known Google Apps domain, provide the
|
||
|
domain name ('example.com') here. If not provided, 'default'
|
||
|
will be used, and the user will be prompted to select an account
|
||
|
if they are signed in with a Google Account and Google Apps
|
||
|
accounts.
|
||
|
language: str (optional) An ISO 639 country code identifying what
|
||
|
language the approval page should be translated in (for example,
|
||
|
'en' for English). The default is the user's selected language.
|
||
|
btmpl: str (optional) Forces a mobile version of the approval page. The
|
||
|
only accepted value is 'mobile'.
|
||
|
auth_server: str (optional) The start of the token authorization web
|
||
|
page. Defaults to
|
||
|
'https://www.google.com/accounts/OAuthAuthorizeToken'
|
||
|
"""
|
||
|
return generate_oauth_authorization_url(
|
||
|
self.token, hd=google_apps_domain, hl=language, btmpl=btmpl,
|
||
|
auth_server=auth_server)
|
||
|
|
||
|
GenerateAuthorizationUrl = generate_authorization_url
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets the Authorization header in the HTTP request using the token.
|
||
|
|
||
|
Calculates an HMAC signature using the information in the token to
|
||
|
indicate that the request came from this application and that this
|
||
|
application has permission to access a particular user's data.
|
||
|
|
||
|
Returns:
|
||
|
The same HTTP request object which was passed in.
|
||
|
"""
|
||
|
timestamp = str(int(time.time()))
|
||
|
nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
|
||
|
signature = generate_hmac_signature(
|
||
|
http_request, self.consumer_key, self.consumer_secret, timestamp,
|
||
|
nonce, version='1.0', next=self.next, token=self.token,
|
||
|
token_secret=self.token_secret, verifier=self.verifier)
|
||
|
http_request.headers['Authorization'] = generate_auth_header(
|
||
|
self.consumer_key, timestamp, nonce, HMAC_SHA1, signature,
|
||
|
version='1.0', next=self.next, token=self.token,
|
||
|
verifier=self.verifier)
|
||
|
return http_request
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
class OAuthRsaToken(OAuthHmacToken):
|
||
|
SIGNATURE_METHOD = RSA_SHA1
|
||
|
|
||
|
def __init__(self, consumer_key, rsa_private_key, token, token_secret,
|
||
|
auth_state, next=None, verifier=None):
|
||
|
self.consumer_key = consumer_key
|
||
|
self.rsa_private_key = rsa_private_key
|
||
|
self.token = token
|
||
|
self.token_secret = token_secret
|
||
|
self.auth_state = auth_state
|
||
|
self.next = next
|
||
|
self.verifier = verifier # Used to convert request token to access token.
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets the Authorization header in the HTTP request using the token.
|
||
|
|
||
|
Calculates an RSA signature using the information in the token to
|
||
|
indicate that the request came from this application and that this
|
||
|
application has permission to access a particular user's data.
|
||
|
|
||
|
Returns:
|
||
|
The same HTTP request object which was passed in.
|
||
|
"""
|
||
|
timestamp = str(int(time.time()))
|
||
|
nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
|
||
|
signature = generate_rsa_signature(
|
||
|
http_request, self.consumer_key, self.rsa_private_key, timestamp,
|
||
|
nonce, version='1.0', next=self.next, token=self.token,
|
||
|
token_secret=self.token_secret, verifier=self.verifier)
|
||
|
http_request.headers['Authorization'] = generate_auth_header(
|
||
|
self.consumer_key, timestamp, nonce, RSA_SHA1, signature,
|
||
|
version='1.0', next=self.next, token=self.token,
|
||
|
verifier=self.verifier)
|
||
|
return http_request
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
class TwoLeggedOAuthHmacToken(OAuthHmacToken):
|
||
|
|
||
|
def __init__(self, consumer_key, consumer_secret, requestor_id):
|
||
|
self.requestor_id = requestor_id
|
||
|
OAuthHmacToken.__init__(
|
||
|
self, consumer_key, consumer_secret, None, None, ACCESS_TOKEN,
|
||
|
next=None, verifier=None)
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets the Authorization header in the HTTP request using the token.
|
||
|
|
||
|
Calculates an HMAC signature using the information in the token to
|
||
|
indicate that the request came from this application and that this
|
||
|
application has permission to access a particular user's data using 2LO.
|
||
|
|
||
|
Returns:
|
||
|
The same HTTP request object which was passed in.
|
||
|
"""
|
||
|
http_request.uri.query['xoauth_requestor_id'] = self.requestor_id
|
||
|
return OAuthHmacToken.modify_request(self, http_request)
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
class TwoLeggedOAuthRsaToken(OAuthRsaToken):
|
||
|
|
||
|
def __init__(self, consumer_key, rsa_private_key, requestor_id):
|
||
|
self.requestor_id = requestor_id
|
||
|
OAuthRsaToken.__init__(
|
||
|
self, consumer_key, rsa_private_key, None, None, ACCESS_TOKEN,
|
||
|
next=None, verifier=None)
|
||
|
|
||
|
def modify_request(self, http_request):
|
||
|
"""Sets the Authorization header in the HTTP request using the token.
|
||
|
|
||
|
Calculates an RSA signature using the information in the token to
|
||
|
indicate that the request came from this application and that this
|
||
|
application has permission to access a particular user's data using 2LO.
|
||
|
|
||
|
Returns:
|
||
|
The same HTTP request object which was passed in.
|
||
|
"""
|
||
|
http_request.uri.query['xoauth_requestor_id'] = self.requestor_id
|
||
|
return OAuthRsaToken.modify_request(self, http_request)
|
||
|
|
||
|
ModifyRequest = modify_request
|
||
|
|
||
|
|
||
|
def _join_token_parts(*args):
|
||
|
""""Escapes and combines all strings passed in.
|
||
|
|
||
|
Used to convert a token object's members into a string instead of
|
||
|
using pickle.
|
||
|
|
||
|
Note: A None value will be converted to an empty string.
|
||
|
|
||
|
Returns:
|
||
|
A string in the form 1x|member1|member2|member3...
|
||
|
"""
|
||
|
return '|'.join([urllib.quote_plus(a or '') for a in args])
|
||
|
|
||
|
|
||
|
def _split_token_parts(blob):
|
||
|
"""Extracts and unescapes fields from the provided binary string.
|
||
|
|
||
|
Reverses the packing performed by _join_token_parts. Used to extract
|
||
|
the members of a token object.
|
||
|
|
||
|
Note: An empty string from the blob will be interpreted as None.
|
||
|
|
||
|
Args:
|
||
|
blob: str A string of the form 1x|member1|member2|member3 as created
|
||
|
by _join_token_parts
|
||
|
|
||
|
Returns:
|
||
|
A list of unescaped strings.
|
||
|
"""
|
||
|
return [urllib.unquote_plus(part) or None for part in blob.split('|')]
|
||
|
|
||
|
|
||
|
def token_to_blob(token):
|
||
|
"""Serializes the token data as a string for storage in a datastore.
|
||
|
|
||
|
Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
|
||
|
OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
|
||
|
TwoLeggedOAuthHmacToken.
|
||
|
|
||
|
Args:
|
||
|
token: A token object which must be of one of the supported token classes.
|
||
|
|
||
|
Raises:
|
||
|
UnsupportedTokenType if the token is not one of the supported token
|
||
|
classes listed above.
|
||
|
|
||
|
Returns:
|
||
|
A string represenging this token. The string can be converted back into
|
||
|
an equivalent token object using token_from_blob. Note that any members
|
||
|
which are set to '' will be set to None when the token is deserialized
|
||
|
by token_from_blob.
|
||
|
"""
|
||
|
if isinstance(token, ClientLoginToken):
|
||
|
return _join_token_parts('1c', token.token_string)
|
||
|
# Check for secure auth sub type first since it is a subclass of
|
||
|
# AuthSubToken.
|
||
|
elif isinstance(token, SecureAuthSubToken):
|
||
|
return _join_token_parts('1s', token.token_string, token.rsa_private_key,
|
||
|
*token.scopes)
|
||
|
elif isinstance(token, AuthSubToken):
|
||
|
return _join_token_parts('1a', token.token_string, *token.scopes)
|
||
|
elif isinstance(token, TwoLeggedOAuthRsaToken):
|
||
|
return _join_token_parts(
|
||
|
'1rtl', token.consumer_key, token.rsa_private_key, token.requestor_id)
|
||
|
elif isinstance(token, TwoLeggedOAuthHmacToken):
|
||
|
return _join_token_parts(
|
||
|
'1htl', token.consumer_key, token.consumer_secret, token.requestor_id)
|
||
|
# Check RSA OAuth token first since the OAuthRsaToken is a subclass of
|
||
|
# OAuthHmacToken.
|
||
|
elif isinstance(token, OAuthRsaToken):
|
||
|
return _join_token_parts(
|
||
|
'1r', token.consumer_key, token.rsa_private_key, token.token,
|
||
|
token.token_secret, str(token.auth_state), token.next,
|
||
|
token.verifier)
|
||
|
elif isinstance(token, OAuthHmacToken):
|
||
|
return _join_token_parts(
|
||
|
'1h', token.consumer_key, token.consumer_secret, token.token,
|
||
|
token.token_secret, str(token.auth_state), token.next,
|
||
|
token.verifier)
|
||
|
else:
|
||
|
raise UnsupportedTokenType(
|
||
|
'Unable to serialize token of type %s' % type(token))
|
||
|
|
||
|
|
||
|
TokenToBlob = token_to_blob
|
||
|
|
||
|
|
||
|
def token_from_blob(blob):
|
||
|
"""Deserializes a token string from the datastore back into a token object.
|
||
|
|
||
|
Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
|
||
|
OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
|
||
|
TwoLeggedOAuthHmacToken.
|
||
|
|
||
|
Args:
|
||
|
blob: string created by token_to_blob.
|
||
|
|
||
|
Raises:
|
||
|
UnsupportedTokenType if the token is not one of the supported token
|
||
|
classes listed above.
|
||
|
|
||
|
Returns:
|
||
|
A new token object with members set to the values serialized in the
|
||
|
blob string. Note that any members which were set to '' in the original
|
||
|
token will now be None.
|
||
|
"""
|
||
|
parts = _split_token_parts(blob)
|
||
|
if parts[0] == '1c':
|
||
|
return ClientLoginToken(parts[1])
|
||
|
elif parts[0] == '1a':
|
||
|
return AuthSubToken(parts[1], parts[2:])
|
||
|
elif parts[0] == '1s':
|
||
|
return SecureAuthSubToken(parts[1], parts[2], parts[3:])
|
||
|
elif parts[0] == '1rtl':
|
||
|
return TwoLeggedOAuthRsaToken(parts[1], parts[2], parts[3])
|
||
|
elif parts[0] == '1htl':
|
||
|
return TwoLeggedOAuthHmacToken(parts[1], parts[2], parts[3])
|
||
|
elif parts[0] == '1r':
|
||
|
auth_state = int(parts[5])
|
||
|
return OAuthRsaToken(parts[1], parts[2], parts[3], parts[4], auth_state,
|
||
|
parts[6], parts[7])
|
||
|
elif parts[0] == '1h':
|
||
|
auth_state = int(parts[5])
|
||
|
return OAuthHmacToken(parts[1], parts[2], parts[3], parts[4], auth_state,
|
||
|
parts[6], parts[7])
|
||
|
else:
|
||
|
raise UnsupportedTokenType(
|
||
|
'Unable to deserialize token with type marker of %s' % parts[0])
|
||
|
|
||
|
|
||
|
TokenFromBlob = token_from_blob
|
||
|
|
||
|
|
||
|
def dump_tokens(tokens):
|
||
|
return ','.join([token_to_blob(t) for t in tokens])
|
||
|
|
||
|
|
||
|
def load_tokens(blob):
|
||
|
return [token_from_blob(s) for s in blob.split(',')]
|
||
|
|
||
|
|
||
|
def find_scopes_for_services(service_names=None):
|
||
|
"""Creates a combined list of scope URLs for the desired services.
|
||
|
|
||
|
This method searches the AUTH_SCOPES dictionary.
|
||
|
|
||
|
Args:
|
||
|
service_names: list of strings (optional) Each name must be a key in the
|
||
|
AUTH_SCOPES dictionary. If no list is provided (None) then
|
||
|
the resulting list will contain all scope URLs in the
|
||
|
AUTH_SCOPES dict.
|
||
|
|
||
|
Returns:
|
||
|
A list of URL strings which are the scopes needed to access these services
|
||
|
when requesting a token using AuthSub or OAuth.
|
||
|
"""
|
||
|
result_scopes = []
|
||
|
if service_names is None:
|
||
|
for service_name, scopes in AUTH_SCOPES.iteritems():
|
||
|
result_scopes.extend(scopes)
|
||
|
else:
|
||
|
for service_name in service_names:
|
||
|
result_scopes.extend(AUTH_SCOPES[service_name])
|
||
|
return result_scopes
|
||
|
|
||
|
|
||
|
FindScopesForServices = find_scopes_for_services
|
||
|
|
||
|
|
||
|
def ae_save(token, token_key):
|
||
|
"""Stores an auth token in the App Engine datastore.
|
||
|
|
||
|
This is a convenience method for using the library with App Engine.
|
||
|
Recommended usage is to associate the auth token with the current_user.
|
||
|
If a user is signed in to the app using the App Engine users API, you
|
||
|
can use
|
||
|
gdata.gauth.ae_save(some_token, users.get_current_user().user_id())
|
||
|
If you are not using the Users API you are free to choose whatever
|
||
|
string you would like for a token_string.
|
||
|
|
||
|
Args:
|
||
|
token: an auth token object. Must be one of ClientLoginToken,
|
||
|
AuthSubToken, SecureAuthSubToken, OAuthRsaToken, or OAuthHmacToken
|
||
|
(see token_to_blob).
|
||
|
token_key: str A unique identified to be used when you want to retrieve
|
||
|
the token. If the user is signed in to App Engine using the
|
||
|
users API, I recommend using the user ID for the token_key:
|
||
|
users.get_current_user().user_id()
|
||
|
"""
|
||
|
import gdata.alt.app_engine
|
||
|
key_name = ''.join(('gd_auth_token', token_key))
|
||
|
return gdata.alt.app_engine.set_token(key_name, token_to_blob(token))
|
||
|
|
||
|
|
||
|
AeSave = ae_save
|
||
|
|
||
|
|
||
|
def ae_load(token_key):
|
||
|
"""Retrieves a token object from the App Engine datastore.
|
||
|
|
||
|
This is a convenience method for using the library with App Engine.
|
||
|
See also ae_save.
|
||
|
|
||
|
Args:
|
||
|
token_key: str The unique key associated with the desired token when it
|
||
|
was saved using ae_save.
|
||
|
|
||
|
Returns:
|
||
|
A token object if there was a token associated with the token_key or None
|
||
|
if the key could not be found.
|
||
|
"""
|
||
|
import gdata.alt.app_engine
|
||
|
key_name = ''.join(('gd_auth_token', token_key))
|
||
|
token_string = gdata.alt.app_engine.get_token(key_name)
|
||
|
if token_string is not None:
|
||
|
return token_from_blob(token_string)
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
AeLoad = ae_load
|
||
|
|
||
|
|
||
|
def ae_delete(token_key):
|
||
|
"""Removes the token object from the App Engine datastore."""
|
||
|
import gdata.alt.app_engine
|
||
|
key_name = ''.join(('gd_auth_token', token_key))
|
||
|
gdata.alt.app_engine.delete_token(key_name)
|
||
|
|
||
|
|
||
|
AeDelete = ae_delete
|