953 lines
37 KiB
Python
953 lines
37 KiB
Python
|
#!/usr/bin/python
|
||
|
#
|
||
|
# Copyright (C) 2007 - 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.
|
||
|
|
||
|
|
||
|
import cgi
|
||
|
import math
|
||
|
import random
|
||
|
import re
|
||
|
import time
|
||
|
import types
|
||
|
import urllib
|
||
|
import atom.http_interface
|
||
|
import atom.token_store
|
||
|
import atom.url
|
||
|
import gdata.oauth as oauth
|
||
|
import gdata.oauth.rsa as oauth_rsa
|
||
|
import gdata.tlslite.utils.keyfactory as keyfactory
|
||
|
import gdata.tlslite.utils.cryptomath as cryptomath
|
||
|
|
||
|
import gdata.gauth
|
||
|
|
||
|
__author__ = 'api.jscudder (Jeff Scudder)'
|
||
|
|
||
|
|
||
|
PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
|
||
|
AUTHSUB_AUTH_LABEL = 'AuthSub token='
|
||
|
|
||
|
|
||
|
"""This module provides functions and objects used with Google authentication.
|
||
|
|
||
|
Details on Google authorization mechanisms used with the Google Data APIs can
|
||
|
be found here:
|
||
|
http://code.google.com/apis/gdata/auth.html
|
||
|
http://code.google.com/apis/accounts/
|
||
|
|
||
|
The essential functions are the following.
|
||
|
Related to ClientLogin:
|
||
|
generate_client_login_request_body: Constructs the body of an HTTP request to
|
||
|
obtain a ClientLogin token for a specific
|
||
|
service.
|
||
|
extract_client_login_token: Creates a ClientLoginToken with the token from a
|
||
|
success response to a ClientLogin request.
|
||
|
get_captcha_challenge: If the server responded to the ClientLogin request
|
||
|
with a CAPTCHA challenge, this method extracts the
|
||
|
CAPTCHA URL and identifying CAPTCHA token.
|
||
|
|
||
|
Related to AuthSub:
|
||
|
generate_auth_sub_url: Constructs a full URL for a AuthSub request. The
|
||
|
user's browser must be sent to this Google Accounts
|
||
|
URL and redirected back to the app to obtain the
|
||
|
AuthSub token.
|
||
|
extract_auth_sub_token_from_url: Once the user's browser has been
|
||
|
redirected back to the web app, use this
|
||
|
function to create an AuthSubToken with
|
||
|
the correct authorization token and scope.
|
||
|
token_from_http_body: Extracts the AuthSubToken value string from the
|
||
|
server's response to an AuthSub session token upgrade
|
||
|
request.
|
||
|
"""
|
||
|
|
||
|
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.
|
||
|
"""
|
||
|
return gdata.gauth.generate_client_login_request_body(email, password,
|
||
|
service, source, account_type, captcha_token, captcha_response)
|
||
|
|
||
|
|
||
|
GenerateClientLoginRequestBody = generate_client_login_request_body
|
||
|
|
||
|
|
||
|
def GenerateClientLoginAuthToken(http_body):
|
||
|
"""Returns the token value to use in Authorization headers.
|
||
|
|
||
|
Reads the token from the server's response to a Client Login request and
|
||
|
creates header value to use in requests.
|
||
|
|
||
|
Args:
|
||
|
http_body: str The body of the server's HTTP response to a Client Login
|
||
|
request
|
||
|
|
||
|
Returns:
|
||
|
The value half of an Authorization header.
|
||
|
"""
|
||
|
token = get_client_login_token(http_body)
|
||
|
if token:
|
||
|
return 'GoogleLogin auth=%s' % token
|
||
|
return None
|
||
|
|
||
|
|
||
|
def get_client_login_token(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.
|
||
|
"""
|
||
|
return gdata.gauth.get_client_login_token_string(http_body)
|
||
|
|
||
|
|
||
|
def extract_client_login_token(http_body, scopes):
|
||
|
"""Parses the server's response and returns a ClientLoginToken.
|
||
|
|
||
|
Args:
|
||
|
http_body: str The body of the server's HTTP response to a Client Login
|
||
|
request. It is assumed that the login request was successful.
|
||
|
scopes: list containing atom.url.Urls or strs. The scopes list contains
|
||
|
all of the partial URLs under which the client login token is
|
||
|
valid. For example, if scopes contains ['http://example.com/foo']
|
||
|
then the client login token would be valid for
|
||
|
http://example.com/foo/bar/baz
|
||
|
|
||
|
Returns:
|
||
|
A ClientLoginToken which is valid for the specified scopes.
|
||
|
"""
|
||
|
token_string = get_client_login_token(http_body)
|
||
|
token = ClientLoginToken(scopes=scopes)
|
||
|
token.set_token_string(token_string)
|
||
|
return token
|
||
|
|
||
|
|
||
|
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.
|
||
|
"""
|
||
|
return gdata.gauth.get_captcha_challenge(http_body, captcha_base_url)
|
||
|
|
||
|
|
||
|
GetCaptchaChallenge = get_captcha_challenge
|
||
|
|
||
|
|
||
|
def GenerateOAuthRequestTokenUrl(
|
||
|
oauth_input_params, scopes,
|
||
|
request_token_url='https://www.google.com/accounts/OAuthGetRequestToken',
|
||
|
extra_parameters=None):
|
||
|
"""Generate a URL at which a request for OAuth request token is to be sent.
|
||
|
|
||
|
Args:
|
||
|
oauth_input_params: OAuthInputParams OAuth input parameters.
|
||
|
scopes: list of strings The URLs of the services to be accessed.
|
||
|
request_token_url: string The beginning of the request token URL. This is
|
||
|
normally 'https://www.google.com/accounts/OAuthGetRequestToken' or
|
||
|
'/accounts/OAuthGetRequestToken'
|
||
|
extra_parameters: dict (optional) key-value pairs as any additional
|
||
|
parameters to be included in the URL and signature while making a
|
||
|
request for fetching an OAuth request token. All the OAuth parameters
|
||
|
are added by default. But if provided through this argument, any
|
||
|
default parameters will be overwritten. For e.g. a default parameter
|
||
|
oauth_version 1.0 can be overwritten if
|
||
|
extra_parameters = {'oauth_version': '2.0'}
|
||
|
|
||
|
Returns:
|
||
|
atom.url.Url OAuth request token URL.
|
||
|
"""
|
||
|
scopes_string = ' '.join([str(scope) for scope in scopes])
|
||
|
parameters = {'scope': scopes_string}
|
||
|
if extra_parameters:
|
||
|
parameters.update(extra_parameters)
|
||
|
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
|
||
|
oauth_input_params.GetConsumer(), http_url=request_token_url,
|
||
|
parameters=parameters)
|
||
|
oauth_request.sign_request(oauth_input_params.GetSignatureMethod(),
|
||
|
oauth_input_params.GetConsumer(), None)
|
||
|
return atom.url.parse_url(oauth_request.to_url())
|
||
|
|
||
|
|
||
|
def GenerateOAuthAuthorizationUrl(
|
||
|
request_token,
|
||
|
authorization_url='https://www.google.com/accounts/OAuthAuthorizeToken',
|
||
|
callback_url=None, extra_params=None,
|
||
|
include_scopes_in_callback=False, scopes_param_prefix='oauth_token_scope'):
|
||
|
"""Generates URL at which user will login to authorize the request token.
|
||
|
|
||
|
Args:
|
||
|
request_token: gdata.auth.OAuthToken OAuth request token.
|
||
|
authorization_url: string The beginning of the authorization URL. This is
|
||
|
normally 'https://www.google.com/accounts/OAuthAuthorizeToken' or
|
||
|
'/accounts/OAuthAuthorizeToken'
|
||
|
callback_url: string (optional) The URL user will be sent to after
|
||
|
logging in and granting access.
|
||
|
extra_params: dict (optional) Additional parameters to be sent.
|
||
|
include_scopes_in_callback: Boolean (default=False) if set to True, and
|
||
|
if 'callback_url' is present, the 'callback_url' will be modified to
|
||
|
include the scope(s) from the request token as a URL parameter. The
|
||
|
key for the 'callback' URL's scope parameter will be
|
||
|
OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as
|
||
|
a parameter to the 'callback' URL, is that the page which receives
|
||
|
the OAuth token will be able to tell which URLs the token grants
|
||
|
access to.
|
||
|
scopes_param_prefix: string (default='oauth_token_scope') The URL
|
||
|
parameter key which maps to the list of valid scopes for the token.
|
||
|
This URL parameter will be included in the callback URL along with
|
||
|
the scopes of the token as value if include_scopes_in_callback=True.
|
||
|
|
||
|
Returns:
|
||
|
atom.url.Url OAuth authorization URL.
|
||
|
"""
|
||
|
scopes = request_token.scopes
|
||
|
if isinstance(scopes, list):
|
||
|
scopes = ' '.join(scopes)
|
||
|
if include_scopes_in_callback and callback_url:
|
||
|
if callback_url.find('?') > -1:
|
||
|
callback_url += '&'
|
||
|
else:
|
||
|
callback_url += '?'
|
||
|
callback_url += urllib.urlencode({scopes_param_prefix:scopes})
|
||
|
oauth_token = oauth.OAuthToken(request_token.key, request_token.secret)
|
||
|
oauth_request = oauth.OAuthRequest.from_token_and_callback(
|
||
|
token=oauth_token, callback=callback_url,
|
||
|
http_url=authorization_url, parameters=extra_params)
|
||
|
return atom.url.parse_url(oauth_request.to_url())
|
||
|
|
||
|
|
||
|
def GenerateOAuthAccessTokenUrl(
|
||
|
authorized_request_token,
|
||
|
oauth_input_params,
|
||
|
access_token_url='https://www.google.com/accounts/OAuthGetAccessToken',
|
||
|
oauth_version='1.0',
|
||
|
oauth_verifier=None):
|
||
|
"""Generates URL at which user will login to authorize the request token.
|
||
|
|
||
|
Args:
|
||
|
authorized_request_token: gdata.auth.OAuthToken OAuth authorized request
|
||
|
token.
|
||
|
oauth_input_params: OAuthInputParams OAuth input parameters.
|
||
|
access_token_url: string The beginning of the authorization URL. This is
|
||
|
normally 'https://www.google.com/accounts/OAuthGetAccessToken' or
|
||
|
'/accounts/OAuthGetAccessToken'
|
||
|
oauth_version: str (default='1.0') oauth_version parameter.
|
||
|
oauth_verifier: str (optional) If present, it is assumed that the client
|
||
|
will use the OAuth v1.0a protocol which includes passing the
|
||
|
oauth_verifier (as returned by the SP) in the access token step.
|
||
|
|
||
|
Returns:
|
||
|
atom.url.Url OAuth access token URL.
|
||
|
"""
|
||
|
oauth_token = oauth.OAuthToken(authorized_request_token.key,
|
||
|
authorized_request_token.secret)
|
||
|
parameters = {'oauth_version': oauth_version}
|
||
|
if oauth_verifier is not None:
|
||
|
parameters['oauth_verifier'] = oauth_verifier
|
||
|
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
|
||
|
oauth_input_params.GetConsumer(), token=oauth_token,
|
||
|
http_url=access_token_url, parameters=parameters)
|
||
|
oauth_request.sign_request(oauth_input_params.GetSignatureMethod(),
|
||
|
oauth_input_params.GetConsumer(), oauth_token)
|
||
|
return atom.url.parse_url(oauth_request.to_url())
|
||
|
|
||
|
|
||
|
def GenerateAuthSubUrl(next, scope, secure=False, session=True,
|
||
|
request_url='https://www.google.com/accounts/AuthSubRequest',
|
||
|
domain='default'):
|
||
|
"""Generate a URL at which the user will login and be redirected back.
|
||
|
|
||
|
Users enter their credentials on a Google login page and a token is sent
|
||
|
to the URL specified in next. See documentation for AuthSub login at:
|
||
|
http://code.google.com/apis/accounts/AuthForWebApps.html
|
||
|
|
||
|
Args:
|
||
|
request_url: str The beginning of the request URL. This is normally
|
||
|
'http://www.google.com/accounts/AuthSubRequest' or
|
||
|
'/accounts/AuthSubRequest'
|
||
|
next: string The URL user will be sent to after logging in.
|
||
|
scope: string The URL of the service to be accessed.
|
||
|
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.
|
||
|
domain: str (optional) The Google Apps domain for this account. If this
|
||
|
is not a Google Apps account, use 'default' which is the default
|
||
|
value.
|
||
|
"""
|
||
|
# Translate True/False values for parameters into numeric values acceoted
|
||
|
# by the AuthSub service.
|
||
|
if secure:
|
||
|
secure = 1
|
||
|
else:
|
||
|
secure = 0
|
||
|
|
||
|
if session:
|
||
|
session = 1
|
||
|
else:
|
||
|
session = 0
|
||
|
|
||
|
request_params = urllib.urlencode({'next': next, 'scope': scope,
|
||
|
'secure': secure, 'session': session,
|
||
|
'hd': domain})
|
||
|
if request_url.find('?') == -1:
|
||
|
return '%s?%s' % (request_url, request_params)
|
||
|
else:
|
||
|
# The request URL already contained url parameters so we should add
|
||
|
# the parameters using the & seperator
|
||
|
return '%s&%s' % (request_url, request_params)
|
||
|
|
||
|
|
||
|
def generate_auth_sub_url(next, scopes, secure=False, session=True,
|
||
|
request_url='https://www.google.com/accounts/AuthSubRequest',
|
||
|
domain='default', scopes_param_prefix='auth_sub_scopes'):
|
||
|
"""Constructs a URL string 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 can be
|
||
|
extracted from the request URL.
|
||
|
|
||
|
Args:
|
||
|
next: atom.url.URL or string The URL user will be sent to after
|
||
|
authorizing this web application to access their data.
|
||
|
scopes: list containint strings The URLs of the services to be accessed.
|
||
|
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.url.Url 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.url.Url 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.url.parse_url(next)
|
||
|
scopes_string = ' '.join([str(scope) for scope in scopes])
|
||
|
next.params[scopes_param_prefix] = scopes_string
|
||
|
|
||
|
if isinstance(request_url, (str, unicode)):
|
||
|
request_url = atom.url.parse_url(request_url)
|
||
|
request_url.params['next'] = str(next)
|
||
|
request_url.params['scope'] = scopes_string
|
||
|
if session:
|
||
|
request_url.params['session'] = 1
|
||
|
else:
|
||
|
request_url.params['session'] = 0
|
||
|
if secure:
|
||
|
request_url.params['secure'] = 1
|
||
|
else:
|
||
|
request_url.params['secure'] = 0
|
||
|
request_url.params['hd'] = domain
|
||
|
return request_url
|
||
|
|
||
|
|
||
|
def AuthSubTokenFromUrl(url):
|
||
|
"""Extracts the AuthSub token from the URL.
|
||
|
|
||
|
Used after the AuthSub redirect has sent the user to the 'next' page and
|
||
|
appended the token to the URL. This function returns the value to be used
|
||
|
in the Authorization header.
|
||
|
|
||
|
Args:
|
||
|
url: str The URL of the current page which contains the AuthSub token as
|
||
|
a URL parameter.
|
||
|
"""
|
||
|
token = TokenFromUrl(url)
|
||
|
if token:
|
||
|
return 'AuthSub token=%s' % token
|
||
|
return None
|
||
|
|
||
|
|
||
|
def TokenFromUrl(url):
|
||
|
"""Extracts the AuthSub token from the URL.
|
||
|
|
||
|
Returns the raw token value.
|
||
|
|
||
|
Args:
|
||
|
url: str The URL or the query portion of the URL string (after the ?) of
|
||
|
the current page which contains the AuthSub token as a URL parameter.
|
||
|
"""
|
||
|
if url.find('?') > -1:
|
||
|
query_params = url.split('?')[1]
|
||
|
else:
|
||
|
query_params = url
|
||
|
for pair in query_params.split('&'):
|
||
|
if pair.startswith('token='):
|
||
|
return pair[6:]
|
||
|
return None
|
||
|
|
||
|
|
||
|
def extract_auth_sub_token_from_url(url,
|
||
|
scopes_param_prefix='auth_sub_scopes', rsa_key=None):
|
||
|
"""Creates an AuthSubToken and sets the token value and scopes from the URL.
|
||
|
|
||
|
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:
|
||
|
An AuthSubToken with the token value from the URL and set to be valid for
|
||
|
the scopes passed in on the URL. If no scopes were included in the URL,
|
||
|
the AuthSubToken defaults to being valid for no scopes. If there was no
|
||
|
'token' parameter in the URL, this function returns None.
|
||
|
"""
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.url.parse_url(url)
|
||
|
if 'token' not in url.params:
|
||
|
return None
|
||
|
scopes = []
|
||
|
if scopes_param_prefix in url.params:
|
||
|
scopes = url.params[scopes_param_prefix].split(' ')
|
||
|
token_value = url.params['token']
|
||
|
if rsa_key:
|
||
|
token = SecureAuthSubToken(rsa_key, scopes=scopes)
|
||
|
else:
|
||
|
token = AuthSubToken(scopes=scopes)
|
||
|
token.set_token_string(token_value)
|
||
|
return token
|
||
|
|
||
|
|
||
|
def AuthSubTokenFromHttpBody(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 header value to use for Authorization which contains the AuthSub
|
||
|
token.
|
||
|
"""
|
||
|
token_value = token_from_http_body(http_body)
|
||
|
if token_value:
|
||
|
return '%s%s' % (AUTHSUB_AUTH_LABEL, token_value)
|
||
|
return None
|
||
|
|
||
|
|
||
|
def token_from_http_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 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
|
||
|
|
||
|
|
||
|
TokenFromHttpBody = token_from_http_body
|
||
|
|
||
|
|
||
|
def OAuthTokenFromUrl(url, scopes_param_prefix='oauth_token_scope'):
|
||
|
"""Creates an OAuthToken and sets token key and scopes (if present) from URL.
|
||
|
|
||
|
After the Google Accounts OAuth pages redirect the user's broswer back to
|
||
|
the web application (using the 'callback' URL from the request) the web app
|
||
|
can extract the token from the current page's URL. The token is same as the
|
||
|
request token, but it is either authorized (if user grants access) or
|
||
|
unauthorized (if user denies access). The token is provided as a
|
||
|
URL parameter named 'oauth_token' and if it was chosen to use
|
||
|
GenerateOAuthAuthorizationUrl with include_scopes_in_param=True, 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:
|
||
|
An OAuthToken with the token key from the URL and set to be valid for
|
||
|
the scopes passed in on the URL. If no scopes were included in the URL,
|
||
|
the OAuthToken defaults to being valid for no scopes. If there was no
|
||
|
'oauth_token' parameter in the URL, this function returns None.
|
||
|
"""
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.url.parse_url(url)
|
||
|
if 'oauth_token' not in url.params:
|
||
|
return None
|
||
|
scopes = []
|
||
|
if scopes_param_prefix in url.params:
|
||
|
scopes = url.params[scopes_param_prefix].split(' ')
|
||
|
token_key = url.params['oauth_token']
|
||
|
token = OAuthToken(key=token_key, scopes=scopes)
|
||
|
return token
|
||
|
|
||
|
|
||
|
def OAuthTokenFromHttpBody(http_body):
|
||
|
"""Parses the HTTP response body and returns an OAuth token.
|
||
|
|
||
|
The returned OAuth token will just have key and secret parameters set.
|
||
|
It won't have any knowledge about the scopes or oauth_input_params. It is
|
||
|
your responsibility to make it aware of the remaining parameters.
|
||
|
|
||
|
Returns:
|
||
|
OAuthToken OAuth token.
|
||
|
"""
|
||
|
token = oauth.OAuthToken.from_string(http_body)
|
||
|
oauth_token = OAuthToken(key=token.key, secret=token.secret)
|
||
|
return oauth_token
|
||
|
|
||
|
|
||
|
class OAuthSignatureMethod(object):
|
||
|
"""Holds valid OAuth signature methods.
|
||
|
|
||
|
RSA_SHA1: Class to build signature according to RSA-SHA1 algorithm.
|
||
|
HMAC_SHA1: Class to build signature according to HMAC-SHA1 algorithm.
|
||
|
"""
|
||
|
|
||
|
HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1
|
||
|
|
||
|
class RSA_SHA1(oauth_rsa.OAuthSignatureMethod_RSA_SHA1):
|
||
|
"""Provides implementation for abstract methods to return RSA certs."""
|
||
|
|
||
|
def __init__(self, private_key, public_cert):
|
||
|
self.private_key = private_key
|
||
|
self.public_cert = public_cert
|
||
|
|
||
|
def _fetch_public_cert(self, unused_oauth_request):
|
||
|
return self.public_cert
|
||
|
|
||
|
def _fetch_private_cert(self, unused_oauth_request):
|
||
|
return self.private_key
|
||
|
|
||
|
|
||
|
class OAuthInputParams(object):
|
||
|
"""Stores OAuth input parameters.
|
||
|
|
||
|
This class is a store for OAuth input parameters viz. consumer key and secret,
|
||
|
signature method and RSA key.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, signature_method, consumer_key, consumer_secret=None,
|
||
|
rsa_key=None, requestor_id=None):
|
||
|
"""Initializes object with parameters required for using OAuth mechanism.
|
||
|
|
||
|
NOTE: Though consumer_secret and rsa_key are optional, either of the two
|
||
|
is required depending on the value of the signature_method.
|
||
|
|
||
|
Args:
|
||
|
signature_method: class which provides implementation for strategy class
|
||
|
oauth.oauth.OAuthSignatureMethod. Signature method to be used for
|
||
|
signing each request. Valid implementations are provided as the
|
||
|
constants defined by gdata.auth.OAuthSignatureMethod. Currently
|
||
|
they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and
|
||
|
gdata.auth.OAuthSignatureMethod.HMAC_SHA1. Instead of passing in
|
||
|
the strategy class, you may pass in a string for 'RSA_SHA1' or
|
||
|
'HMAC_SHA1'. If you plan to use OAuth on App Engine (or another
|
||
|
WSGI environment) I recommend specifying signature method using a
|
||
|
string (the only options are 'RSA_SHA1' and 'HMAC_SHA1'). In these
|
||
|
environments there are sometimes issues with pickling an object in
|
||
|
which a member references a class or function. Storing a string to
|
||
|
refer to the signature method mitigates complications when
|
||
|
pickling.
|
||
|
consumer_key: string Domain identifying third_party web application.
|
||
|
consumer_secret: string (optional) Secret generated during registration.
|
||
|
Required only for HMAC_SHA1 signature method.
|
||
|
rsa_key: string (optional) Private key required for RSA_SHA1 signature
|
||
|
method.
|
||
|
requestor_id: string (optional) User email adress to make requests on
|
||
|
their behalf. This parameter should only be set when performing
|
||
|
2 legged OAuth requests.
|
||
|
"""
|
||
|
if (signature_method == OAuthSignatureMethod.RSA_SHA1
|
||
|
or signature_method == 'RSA_SHA1'):
|
||
|
self.__signature_strategy = 'RSA_SHA1'
|
||
|
elif (signature_method == OAuthSignatureMethod.HMAC_SHA1
|
||
|
or signature_method == 'HMAC_SHA1'):
|
||
|
self.__signature_strategy = 'HMAC_SHA1'
|
||
|
else:
|
||
|
self.__signature_strategy = signature_method
|
||
|
self.rsa_key = rsa_key
|
||
|
self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
|
||
|
self.requestor_id = requestor_id
|
||
|
|
||
|
def __get_signature_method(self):
|
||
|
if self.__signature_strategy == 'RSA_SHA1':
|
||
|
return OAuthSignatureMethod.RSA_SHA1(self.rsa_key, None)
|
||
|
elif self.__signature_strategy == 'HMAC_SHA1':
|
||
|
return OAuthSignatureMethod.HMAC_SHA1()
|
||
|
else:
|
||
|
return self.__signature_strategy()
|
||
|
|
||
|
def __set_signature_method(self, signature_method):
|
||
|
if (signature_method == OAuthSignatureMethod.RSA_SHA1
|
||
|
or signature_method == 'RSA_SHA1'):
|
||
|
self.__signature_strategy = 'RSA_SHA1'
|
||
|
elif (signature_method == OAuthSignatureMethod.HMAC_SHA1
|
||
|
or signature_method == 'HMAC_SHA1'):
|
||
|
self.__signature_strategy = 'HMAC_SHA1'
|
||
|
else:
|
||
|
self.__signature_strategy = signature_method
|
||
|
|
||
|
_signature_method = property(__get_signature_method, __set_signature_method,
|
||
|
doc="""Returns object capable of signing the request using RSA of HMAC.
|
||
|
|
||
|
Replaces the _signature_method member to avoid pickle errors.""")
|
||
|
|
||
|
def GetSignatureMethod(self):
|
||
|
"""Gets the OAuth signature method.
|
||
|
|
||
|
Returns:
|
||
|
object of supertype <oauth.oauth.OAuthSignatureMethod>
|
||
|
"""
|
||
|
return self._signature_method
|
||
|
|
||
|
def GetConsumer(self):
|
||
|
"""Gets the OAuth consumer.
|
||
|
|
||
|
Returns:
|
||
|
object of type <oauth.oauth.Consumer>
|
||
|
"""
|
||
|
return self._consumer
|
||
|
|
||
|
|
||
|
class ClientLoginToken(atom.http_interface.GenericToken):
|
||
|
"""Stores the Authorization header in auth_header and adds to requests.
|
||
|
|
||
|
This token will add it's Authorization header to an HTTP request
|
||
|
as it is made. Ths token class is simple but
|
||
|
some Token classes must calculate portions of the Authorization header
|
||
|
based on the request being made, which is why the token is responsible
|
||
|
for making requests via an http_client parameter.
|
||
|
|
||
|
Args:
|
||
|
auth_header: str The value for the Authorization header.
|
||
|
scopes: list of str or atom.url.Url specifying the beginnings of URLs
|
||
|
for which this token can be used. For example, if scopes contains
|
||
|
'http://example.com/foo', then this token can be used for a request to
|
||
|
'http://example.com/foo/bar' but it cannot be used for a request to
|
||
|
'http://example.com/baz'
|
||
|
"""
|
||
|
def __init__(self, auth_header=None, scopes=None):
|
||
|
self.auth_header = auth_header
|
||
|
self.scopes = scopes or []
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.auth_header
|
||
|
|
||
|
def perform_request(self, http_client, operation, url, data=None,
|
||
|
headers=None):
|
||
|
"""Sets the Authorization header and makes the HTTP request."""
|
||
|
if headers is None:
|
||
|
headers = {'Authorization':self.auth_header}
|
||
|
else:
|
||
|
headers['Authorization'] = self.auth_header
|
||
|
return http_client.request(operation, url, data=data, headers=headers)
|
||
|
|
||
|
def get_token_string(self):
|
||
|
"""Removes PROGRAMMATIC_AUTH_LABEL to give just the token value."""
|
||
|
return self.auth_header[len(PROGRAMMATIC_AUTH_LABEL):]
|
||
|
|
||
|
def set_token_string(self, token_string):
|
||
|
self.auth_header = '%s%s' % (PROGRAMMATIC_AUTH_LABEL, token_string)
|
||
|
|
||
|
def valid_for_scope(self, url):
|
||
|
"""Tells the caller if the token authorizes access to the desired URL.
|
||
|
"""
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.url.parse_url(url)
|
||
|
for scope in self.scopes:
|
||
|
if scope == atom.token_store.SCOPE_ALL:
|
||
|
return True
|
||
|
if isinstance(scope, (str, unicode)):
|
||
|
scope = atom.url.parse_url(scope)
|
||
|
if scope == url:
|
||
|
return True
|
||
|
# Check the host and the path, but ignore the port and protocol.
|
||
|
elif scope.host == url.host and not scope.path:
|
||
|
return True
|
||
|
elif scope.host == url.host and scope.path and not url.path:
|
||
|
continue
|
||
|
elif scope.host == url.host and url.path.startswith(scope.path):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
class AuthSubToken(ClientLoginToken):
|
||
|
def get_token_string(self):
|
||
|
"""Removes AUTHSUB_AUTH_LABEL to give just the token value."""
|
||
|
return self.auth_header[len(AUTHSUB_AUTH_LABEL):]
|
||
|
|
||
|
def set_token_string(self, token_string):
|
||
|
self.auth_header = '%s%s' % (AUTHSUB_AUTH_LABEL, token_string)
|
||
|
|
||
|
|
||
|
class OAuthToken(atom.http_interface.GenericToken):
|
||
|
"""Stores the token key, token secret and scopes for which token is valid.
|
||
|
|
||
|
This token adds the authorization header to each request made. It
|
||
|
re-calculates authorization header for every request since the OAuth
|
||
|
signature to be added to the authorization header is dependent on the
|
||
|
request parameters.
|
||
|
|
||
|
Attributes:
|
||
|
key: str The value for the OAuth token i.e. token key.
|
||
|
secret: str The value for the OAuth token secret.
|
||
|
scopes: list of str or atom.url.Url specifying the beginnings of URLs
|
||
|
for which this token can be used. For example, if scopes contains
|
||
|
'http://example.com/foo', then this token can be used for a request to
|
||
|
'http://example.com/foo/bar' but it cannot be used for a request to
|
||
|
'http://example.com/baz'
|
||
|
oauth_input_params: OAuthInputParams OAuth input parameters.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, key=None, secret=None, scopes=None,
|
||
|
oauth_input_params=None):
|
||
|
self.key = key
|
||
|
self.secret = secret
|
||
|
self.scopes = scopes or []
|
||
|
self.oauth_input_params = oauth_input_params
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.get_token_string()
|
||
|
|
||
|
def get_token_string(self):
|
||
|
"""Returns the token string.
|
||
|
|
||
|
The token string returned is of format
|
||
|
oauth_token=[0]&oauth_token_secret=[1], where [0] and [1] are some strings.
|
||
|
|
||
|
Returns:
|
||
|
A token string of format oauth_token=[0]&oauth_token_secret=[1],
|
||
|
where [0] and [1] are some strings. If self.secret is absent, it just
|
||
|
returns oauth_token=[0]. If self.key is absent, it just returns
|
||
|
oauth_token_secret=[1]. If both are absent, it returns None.
|
||
|
"""
|
||
|
if self.key and self.secret:
|
||
|
return urllib.urlencode({'oauth_token': self.key,
|
||
|
'oauth_token_secret': self.secret})
|
||
|
elif self.key:
|
||
|
return 'oauth_token=%s' % self.key
|
||
|
elif self.secret:
|
||
|
return 'oauth_token_secret=%s' % self.secret
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def set_token_string(self, token_string):
|
||
|
"""Sets the token key and secret from the token string.
|
||
|
|
||
|
Args:
|
||
|
token_string: str Token string of form
|
||
|
oauth_token=[0]&oauth_token_secret=[1]. If oauth_token is not present,
|
||
|
self.key will be None. If oauth_token_secret is not present,
|
||
|
self.secret will be None.
|
||
|
"""
|
||
|
token_params = cgi.parse_qs(token_string, keep_blank_values=False)
|
||
|
if 'oauth_token' in token_params:
|
||
|
self.key = token_params['oauth_token'][0]
|
||
|
if 'oauth_token_secret' in token_params:
|
||
|
self.secret = token_params['oauth_token_secret'][0]
|
||
|
|
||
|
def GetAuthHeader(self, http_method, http_url, realm=''):
|
||
|
"""Get the authentication header.
|
||
|
|
||
|
Args:
|
||
|
http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc.
|
||
|
http_url: string or atom.url.Url HTTP URL to which request is made.
|
||
|
realm: string (default='') realm parameter to be included in the
|
||
|
authorization header.
|
||
|
|
||
|
Returns:
|
||
|
dict Header to be sent with every subsequent request after
|
||
|
authentication.
|
||
|
"""
|
||
|
if isinstance(http_url, types.StringTypes):
|
||
|
http_url = atom.url.parse_url(http_url)
|
||
|
header = None
|
||
|
token = None
|
||
|
if self.key or self.secret:
|
||
|
token = oauth.OAuthToken(self.key, self.secret)
|
||
|
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
|
||
|
self.oauth_input_params.GetConsumer(), token=token,
|
||
|
http_url=str(http_url), http_method=http_method,
|
||
|
parameters=http_url.params)
|
||
|
oauth_request.sign_request(self.oauth_input_params.GetSignatureMethod(),
|
||
|
self.oauth_input_params.GetConsumer(), token)
|
||
|
header = oauth_request.to_header(realm=realm)
|
||
|
header['Authorization'] = header['Authorization'].replace('+', '%2B')
|
||
|
return header
|
||
|
|
||
|
def perform_request(self, http_client, operation, url, data=None,
|
||
|
headers=None):
|
||
|
"""Sets the Authorization header and makes the HTTP request."""
|
||
|
if not headers:
|
||
|
headers = {}
|
||
|
if self.oauth_input_params.requestor_id:
|
||
|
url.params['xoauth_requestor_id'] = self.oauth_input_params.requestor_id
|
||
|
headers.update(self.GetAuthHeader(operation, url))
|
||
|
return http_client.request(operation, url, data=data, headers=headers)
|
||
|
|
||
|
def valid_for_scope(self, url):
|
||
|
if isinstance(url, (str, unicode)):
|
||
|
url = atom.url.parse_url(url)
|
||
|
for scope in self.scopes:
|
||
|
if scope == atom.token_store.SCOPE_ALL:
|
||
|
return True
|
||
|
if isinstance(scope, (str, unicode)):
|
||
|
scope = atom.url.parse_url(scope)
|
||
|
if scope == url:
|
||
|
return True
|
||
|
# Check the host and the path, but ignore the port and protocol.
|
||
|
elif scope.host == url.host and not scope.path:
|
||
|
return True
|
||
|
elif scope.host == url.host and scope.path and not url.path:
|
||
|
continue
|
||
|
elif scope.host == url.host and url.path.startswith(scope.path):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
class SecureAuthSubToken(AuthSubToken):
|
||
|
"""Stores the rsa private key, token, and scopes for the secure AuthSub token.
|
||
|
|
||
|
This token adds the authorization header to each request made. It
|
||
|
re-calculates authorization header for every request since the secure AuthSub
|
||
|
signature to be added to the authorization header is dependent on the
|
||
|
request parameters.
|
||
|
|
||
|
Attributes:
|
||
|
rsa_key: string The RSA private key in PEM format that the token will
|
||
|
use to sign requests
|
||
|
token_string: string (optional) The value for the AuthSub token.
|
||
|
scopes: list of str or atom.url.Url specifying the beginnings of URLs
|
||
|
for which this token can be used. For example, if scopes contains
|
||
|
'http://example.com/foo', then this token can be used for a request to
|
||
|
'http://example.com/foo/bar' but it cannot be used for a request to
|
||
|
'http://example.com/baz'
|
||
|
"""
|
||
|
|
||
|
def __init__(self, rsa_key, token_string=None, scopes=None):
|
||
|
self.rsa_key = keyfactory.parsePEMKey(rsa_key)
|
||
|
self.token_string = token_string or ''
|
||
|
self.scopes = scopes or []
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.get_token_string()
|
||
|
|
||
|
def get_token_string(self):
|
||
|
return str(self.token_string)
|
||
|
|
||
|
def set_token_string(self, token_string):
|
||
|
self.token_string = token_string
|
||
|
|
||
|
def GetAuthHeader(self, http_method, http_url):
|
||
|
"""Generates the Authorization header.
|
||
|
|
||
|
The form of the secure AuthSub Authorization header is
|
||
|
Authorization: AuthSub token="token" sigalg="sigalg" data="data" sig="sig"
|
||
|
and data represents a string in the form
|
||
|
data = http_method http_url timestamp nonce
|
||
|
|
||
|
Args:
|
||
|
http_method: string HTTP method i.e. operation e.g. GET, POST, PUT, etc.
|
||
|
http_url: string or atom.url.Url HTTP URL to which request is made.
|
||
|
|
||
|
Returns:
|
||
|
dict Header to be sent with every subsequent request after authentication.
|
||
|
"""
|
||
|
timestamp = int(math.floor(time.time()))
|
||
|
nonce = '%lu' % random.randrange(1, 2**64)
|
||
|
data = '%s %s %d %s' % (http_method, str(http_url), timestamp, nonce)
|
||
|
sig = cryptomath.bytesToBase64(self.rsa_key.hashAndSign(data))
|
||
|
header = {'Authorization': '%s"%s" data="%s" sig="%s" sigalg="rsa-sha1"' %
|
||
|
(AUTHSUB_AUTH_LABEL, self.token_string, data, sig)}
|
||
|
return header
|
||
|
|
||
|
def perform_request(self, http_client, operation, url, data=None,
|
||
|
headers=None):
|
||
|
"""Sets the Authorization header and makes the HTTP request."""
|
||
|
if not headers:
|
||
|
headers = {}
|
||
|
headers.update(self.GetAuthHeader(operation, url))
|
||
|
return http_client.request(operation, url, data=data, headers=headers)
|