Skip to content

Commit 845a545

Browse files
committed
Cleaned up oauth code based on feedback. Created FlowThreeLegged and OAuthCredentials classes.
1 parent 67d7777 commit 845a545

File tree

18 files changed

+2182
-110
lines changed

18 files changed

+2182
-110
lines changed

apiclient/contrib/__init__.py

Whitespace-only changes.

apiclient/ext/__init__.py

Whitespace-only changes.

apiclient/ext/appengine.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright (C) 2010 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Utilities for Google App Engine
16+
17+
Utilities for making it easier to use the
18+
Google API Client for Python on Google App Engine.
19+
"""
20+
21+
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
22+
23+
import pickle
24+
25+
from google.appengine.ext import db
26+
from apiclient.oauth import OAuthCredentials
27+
from apiclient.oauth import FlowThreeLegged
28+
29+
30+
class FlowThreeLeggedProperty(db.Property):
31+
"""Utility property that allows easy
32+
storage and retreival of an
33+
apiclient.oauth.FlowThreeLegged"""
34+
35+
# Tell what the user type is.
36+
data_type = FlowThreeLegged
37+
38+
# For writing to datastore.
39+
def get_value_for_datastore(self, model_instance):
40+
flow = super(FlowThreeLeggedProperty,
41+
self).get_value_for_datastore(model_instance)
42+
return db.Blob(pickle.dumps(flow))
43+
44+
# For reading from datastore.
45+
def make_value_from_datastore(self, value):
46+
if value is None:
47+
return None
48+
return pickle.loads(value)
49+
50+
def validate(self, value):
51+
if value is not None and not isinstance(value, FlowThreeLegged):
52+
raise BadValueError('Property %s must be convertible '
53+
'to a FlowThreeLegged instance (%s)' %
54+
(self.name, value))
55+
return super(FlowThreeLeggedProperty, self).validate(value)
56+
57+
def empty(self, value):
58+
return not value
59+
60+
61+
class OAuthCredentialsProperty(db.Property):
62+
"""Utility property that allows easy
63+
storage and retrieval of
64+
apiclient.oath.OAuthCredentials
65+
"""
66+
67+
# Tell what the user type is.
68+
data_type = OAuthCredentials
69+
70+
# For writing to datastore.
71+
def get_value_for_datastore(self, model_instance):
72+
cred = super(OAuthCredentialsProperty,
73+
self).get_value_for_datastore(model_instance)
74+
return db.Blob(pickle.dumps(cred))
75+
76+
# For reading from datastore.
77+
def make_value_from_datastore(self, value):
78+
if value is None:
79+
return None
80+
return pickle.loads(value)
81+
82+
def validate(self, value):
83+
if value is not None and not isinstance(value, OAuthCredentials):
84+
raise BadValueError('Property %s must be convertible '
85+
'to an OAuthCredentials instance (%s)' %
86+
(self.name, value))
87+
return super(OAuthCredentialsProperty, self).validate(value)
88+
89+
def empty(self, value):
90+
return not value

apiclient/oauth.py

Lines changed: 146 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,24 @@
1010
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
1111

1212
import copy
13-
import urllib
13+
import httplib2
1414
import oauth2 as oauth
15+
import urllib
16+
import logging
1517

1618
try:
1719
from urlparse import parse_qs, parse_qsl
1820
except ImportError:
1921
from cgi import parse_qs, parse_qsl
22+
23+
2024
class MissingParameter(Exception):
2125
pass
2226

23-
def abstract():
24-
raise NotImplementedError("You need to override this function")
25-
2627

27-
class TokenStore(object):
28-
def get(user, service):
29-
"""Returns an oauth.Token based on the (user, service) returning
30-
None if there is no Token for that (user, service).
31-
"""
32-
abstract()
28+
def _abstract():
29+
raise NotImplementedError("You need to override this function")
3330

34-
def set(user, service, token):
35-
abstract()
3631

3732
buzz_discovery = {
3833
'required': ['domain', 'scope'],
@@ -50,7 +45,19 @@ def set(user, service, token):
5045
},
5146
}
5247

48+
5349
def _oauth_uri(name, discovery, params):
50+
"""Look up the OAuth UR from the discovery
51+
document and add query parameters based on
52+
params.
53+
54+
name - The name of the OAuth URI to lookup, one
55+
of 'request', 'access', or 'authorize'.
56+
discovery - Portion of discovery document the describes
57+
the OAuth endpoints.
58+
params - Dictionary that is used to form the query parameters
59+
for the specified URI.
60+
"""
5461
if name not in ['request', 'access', 'authorize']:
5562
raise KeyError(name)
5663
keys = []
@@ -62,9 +69,100 @@ def _oauth_uri(name, discovery, params):
6269
query[key] = params[key]
6370
return discovery[name]['url'] + '?' + urllib.urlencode(query)
6471

65-
class Flow3LO(object):
72+
73+
class Credentials(object):
74+
"""Base class for all Credentials objects.
75+
76+
Subclasses must define an authorize() method
77+
that applies the credentials to an HTTP transport.
78+
"""
79+
80+
def authorize(self, http):
81+
"""Take an httplib2.Http instance (or equivalent) and
82+
authorizes it for the set of credentials, usually by
83+
replacing http.request() with a method that adds in
84+
the appropriate headers and then delegates to the original
85+
Http.request() method.
86+
"""
87+
_abstract()
88+
89+
90+
class OAuthCredentials(Credentials):
91+
"""Credentials object for OAuth 1.0a
92+
"""
93+
94+
def __init__(self, consumer, token, user_agent):
95+
"""
96+
consumer - An instance of oauth.Consumer.
97+
token - An instance of oauth.Token constructed with
98+
the access token and secret.
99+
user_agent - The HTTP User-Agent to provide for this application.
100+
"""
101+
self.consumer = consumer
102+
self.token = token
103+
self.user_agent = user_agent
104+
105+
def authorize(self, http):
106+
"""
107+
Args:
108+
http - An instance of httplib2.Http
109+
or something that acts like it.
110+
111+
Returns:
112+
A modified instance of http that was passed in.
113+
114+
Example:
115+
116+
h = httplib2.Http()
117+
h = credentials.authorize(h)
118+
119+
You can't create a new OAuth
120+
subclass of httplib2.Authenication because
121+
it never gets passed the absolute URI, which is
122+
needed for signing. So instead we have to overload
123+
'request' with a closure that adds in the
124+
Authorization header and then calls the original version
125+
of 'request()'.
126+
"""
127+
request_orig = http.request
128+
signer = oauth.SignatureMethod_HMAC_SHA1()
129+
130+
# The closure that will replace 'httplib2.Http.request'.
131+
def new_request(uri, method="GET", body=None, headers=None,
132+
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
133+
connection_type=None):
134+
"""Modify the request headers to add the appropriate
135+
Authorization header."""
136+
req = oauth.Request.from_consumer_and_token(
137+
self.consumer, self.token, http_method=method, http_url=uri)
138+
req.sign_request(signer, self.consumer, self.token)
139+
if headers == None:
140+
headers = {}
141+
headers.update(req.to_header())
142+
if 'user-agent' not in headers:
143+
headers['user-agent'] = self.user_agent
144+
return request_orig(uri, method, body, headers,
145+
redirections, connection_type)
146+
147+
http.request = new_request
148+
return http
149+
150+
151+
class FlowThreeLegged(object):
152+
"""Does the Three Legged Dance for OAuth 1.0a.
153+
"""
154+
66155
def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
67156
**kwargs):
157+
"""
158+
discovery - Section of the API discovery document that describes
159+
the OAuth endpoints.
160+
consumer_key - OAuth consumer key
161+
consumer_secret - OAuth consumer secret
162+
user_agent - The HTTP User-Agent that identifies the application.
163+
**kwargs - The keyword arguments are all optional and required
164+
parameters for the OAuth calls.
165+
"""
68166
self.discovery = discovery
69167
self.consumer_key = consumer_key
70168
self.consumer_secret = consumer_secret
@@ -75,14 +173,17 @@ def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
75173
if key not in self.params:
76174
raise MissingParameter('Required parameter %s not supplied' % key)
77175

78-
def step1(self, oauth_callback='oob'):
176+
def step1_get_authorize_url(self, oauth_callback='oob'):
79177
"""Returns a URI to redirect to the provider.
80178
81-
If oauth_callback is 'oob' then the next call
82-
should be to step2_pin, otherwise oauth_callback
83-
is a URI and the next call should be to
84-
step2_callback() with the query parameters
85-
received at that callback.
179+
oauth_callback - Either the string 'oob' for a non-web-based application,
180+
or a URI that handles the callback from the authorization
181+
server.
182+
183+
If oauth_callback is 'oob' then pass in the
184+
generated verification code to step2_exchange,
185+
otherwise pass in the query parameters received
186+
at the callback uri to step2_exchange.
86187
"""
87188
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
88189
client = oauth.Client(consumer)
@@ -93,26 +194,36 @@ def step1(self, oauth_callback='oob'):
93194
}
94195
body = urllib.urlencode({'oauth_callback': oauth_callback})
95196
uri = _oauth_uri('request', self.discovery, self.params)
197+
96198
resp, content = client.request(uri, 'POST', headers=headers,
97199
body=body)
98200
if resp['status'] != '200':
99-
print content
201+
logging.error('Failed to retrieve temporary authorization: %s' % content)
100202
raise Exception('Invalid response %s.' % resp['status'])
101203

102204
self.request_token = dict(parse_qsl(content))
103205

104206
auth_params = copy.copy(self.params)
105207
auth_params['oauth_token'] = self.request_token['oauth_token']
106208

107-
uri = _oauth_uri('authorize', self.discovery, auth_params)
108-
return uri
209+
return _oauth_uri('authorize', self.discovery, auth_params)
109210

110-
def step2_pin(self, pin):
111-
"""Returns an oauth_token and oauth_token_secret in a dictionary"""
211+
def step2_exchange(self, verifier):
212+
"""Exhanges an authorized request token
213+
for OAuthCredentials.
214+
215+
verifier - either the verifier token, or a dictionary
216+
of the query parameters to the callback, which contains
217+
the oauth_verifier.
218+
"""
112219

113-
token = oauth.Token(self.request_token['oauth_token'],
220+
if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
221+
verifier = verifier['oauth_verifier']
222+
223+
token = oauth.Token(
224+
self.request_token['oauth_token'],
114225
self.request_token['oauth_token_secret'])
115-
token.set_verifier(pin)
226+
token.set_verifier(verifier)
116227
consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
117228
client = oauth.Client(consumer, token)
118229

@@ -123,8 +234,13 @@ def step2_pin(self, pin):
123234

124235
uri = _oauth_uri('access', self.discovery, self.params)
125236
resp, content = client.request(uri, 'POST', headers=headers)
126-
return dict(parse_qsl(content))
237+
if resp['status'] != '200':
238+
logging.error('Failed to retrieve access token: %s' % content)
239+
raise Exception('Invalid response %s.' % resp['status'])
240+
241+
oauth_params = dict(parse_qsl(content))
242+
token = oauth.Token(
243+
oauth_params['oauth_token'],
244+
oauth_params['oauth_token_secret'])
127245

128-
def step2_callback(self, query_params):
129-
"""Returns an access token via oauth.Token"""
130-
pass
246+
return OAuthCredentials(consumer, token, self.user_agent)

0 commit comments

Comments
 (0)