Skip to content

Commit 8679014

Browse files
author
Jack Diederich
committed
working implementation, better docs
1 parent 9986b97 commit 8679014

File tree

2 files changed

+135
-54
lines changed

2 files changed

+135
-54
lines changed

README.md

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@
33
This is a fork of a fork of a fork. See (http://github.com/OfflineLabs/python-oauth2) for that history. A number of notable differences exist between this code and its forefathers:
44

55
* 0% unit test coverage.
6-
76
* Lightweight at less than 200 lines, including blanks and docstrings.
8-
97
* Completely removed all the OAuth 1.0 code.
10-
118
* Completely removed all non-stdlib dependencies (goodbye httplib2, you won't be missed!).
12-
139
* Implements a later version of the spec than it's parent.
14-
1510
* I'm not sure which version but google's auth2 API accepts this implementation.
1611

1712
# goo.gl URL shortener example
@@ -25,15 +20,50 @@ This is a fork of a fork of a fork. See (http://github.com/OfflineLabs/python-oa
2520

2621
import urlprase
2722
import foauth2
28-
client = foauth2.Client(client_id, client_secret)
23+
client = foauth2.GooglAPI(client_id, client_secret)
2924
print "cut-n-paste the following URL to start the auth dance",
30-
print client.authorization_url(auth_url, redirect_url=redirect_url, scope=scope)
25+
print client.authorization_url()
3126
print "and copy the resulting link below"
3227
answer = raw_input()
3328

3429
query_str = urlparse.urlparse(url_str).query
3530
params = urlparse.parse_qs(query_str, keep_blank_values=True)
3631
code = params['code'][0]
37-
access_token, refresh_token = client.access_token(access_url, code=code, scope=scope)
32+
access_token, refresh_token = client.redeem_token(code=code)
33+
34+
# make a short url
35+
short_url = client.shorten('http://example.com')
36+
print short_url
37+
# get stats
38+
print client.stats(short_url)
39+
40+
GooglAPI inherits from the Client class. You can use the client by itself but
41+
then you have to pass in the URI and scope for the end points as arguments to
42+
the authorize_url and redeem_code functions. GooglAPI sets up the defaults:
43+
44+
class GooglAPI(Client):
45+
user_agent = 'python-foauth2'
46+
# OAuth API
47+
auth_uri = 'https://accounts.google.com/o/oauth2/auth'
48+
refresh_uri = 'https://accounts.google.com/o/oauth2/token'
49+
scope = 'https://www.googleapis.com/auth/urlshortener'
50+
# Shortener API
51+
api_uri = 'https://www.googleapis.com/urlshortener/v1/url'
52+
53+
You are responsible for storing the access and refresh key for later use.
54+
Here is the full (and not very exciting) version of foauth.GooglAPI that
55+
we use. It has a custom redirect URI, includes our goo.gl api key,
56+
and saves the access & refresh tokens to a django model names AuthKey.
57+
Define your own refresh_token() replacement to store stuff where you like.
58+
59+
class GooglAPI(foauth2.GooglAPI):
60+
redirect_uri = 'http://hivefire.com/oauth'
61+
api_uri = 'https://www.googleapis.com/urlshortener/v1/url?key=%s' % GOOGL_KEY
3862

39-
print "now you can use %r to GET short urls from goo.gl" % access_token
63+
def refresh_token(self, *args, **kwargs):
64+
access, refresh = super(GooglAPI, self).refresh_token(*args, **kwargs)
65+
# save the updated access/refresh tokens
66+
AuthKey.objects.filter(service='goo.gl').update(access_token=access,
67+
access_secret=refresh,
68+
acquired_date=datetime.datetime.now())
69+
return access, refresh

foauth2.py

Lines changed: 96 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@
3636
import urllib2
3737
import urlparse
3838

39-
try:
40-
from urlparse import parse_qs, parse_qsl
41-
except ImportError:
42-
from cgi import parse_qs, parse_qsl
43-
4439
try:
4540
import simplejson
4641
except ImportError:
@@ -65,29 +60,32 @@ def __str__(self):
6560

6661
class Client(object):
6762
""" Client for OAuth 2.0 'Bearer Token' """
63+
redirect_uri = None
64+
auth_uri = None
65+
refresh_uri = None
66+
user_agent = None
67+
scope = None
6868

69-
def __init__(self, client_id, client_secret, redirect_uri=None,
70-
timeout=None):
71-
self.client_id = client_id
72-
self.client_secret = client_secret
73-
self.redirect_uri = redirect_uri
74-
self.timeout = timeout
69+
def __init__(self, client_id, client_secret, access_token=None,
70+
refresh_token=None, timeout=None):
7571

76-
if self.client_id is None or self.client_secret is None:
72+
if not client_id or not client_secret:
7773
raise ValueError("Client_id and client_secret must be set.")
7874

79-
@staticmethod
80-
def _split_url_string(param_str):
81-
"""Turn URL string into parameters."""
82-
parameters = parse_qs(param_str, keep_blank_values=False)
83-
for key, val in parameters.iteritems():
84-
parameters[key] = urllib.unquote(val[0])
85-
return parameters
75+
self.client_id = client_id
76+
self.client_secret = client_secret
77+
self.timeout = timeout
78+
self.access_token = access_token
79+
self.refresh_token = refresh_token
8680

87-
def authorization_url(self, uri, redirect_uri=None, scope=None):
81+
def authorization_url(self, auth_uri=None, redirect_uri=None, scope=None):
8882
""" Get the URL to redirect the user for client authorization """
8983
if redirect_uri is None:
9084
redirect_uri = self.redirect_uri
85+
if auth_uri is None:
86+
auth_uri = self.auth_uri
87+
if scope is None:
88+
scope = self.scope
9189

9290
params = {'client_id' : self.client_id,
9391
'redirect_uri' : redirect_uri,
@@ -96,74 +94,127 @@ def authorization_url(self, uri, redirect_uri=None, scope=None):
9694
if scope:
9795
params['scope'] = scope
9896

99-
return '%s?%s' % (uri, urllib.urlencode(params))
97+
return '%s?%s' % (auth_uri, urllib.urlencode(params))
10098

101-
def access_token(self, uri, redirect_uri=None, code=None, scope=None):
99+
def redeem_code(self, refresh_uri=None, redirect_uri=None, code=None, scope=None):
102100
"""Get an access token from the supplied code """
103101

104102
# prepare required args
105103
if code is None:
106104
raise ValueError("Code must be set.")
107105
if redirect_uri is None:
108106
redirect_uri = self.redirect_uri
107+
if refresh_uri is None:
108+
refresh_uri = self.refresh_uri
109+
if scope is None:
110+
scope = self.scope
111+
109112
data = {
110113
'client_id': self.client_id,
111114
'client_secret': self.client_secret,
112115
'code': code,
113116
'redirect_uri': redirect_uri,
114117
'grant_type' : 'authorization_code',
115118
}
119+
116120
if scope is not None:
117121
data['scope'] = scope
118122
body = urllib.urlencode(data)
119123

120-
headers = {'content-type' : 'application/x-www-form-urlencoded',
121-
'user_agent' : 'HiveFire-OAuth2a',
122-
}
124+
headers = {'Content-type' : 'application/x-www-form-urlencoded'}
125+
if self.user_agent:
126+
headers['user-agent'] = self.user_agent
123127

124-
response = self.request(uri, body=body, method='POST', headers=headers)
125-
if not response.code == 200:
128+
response = self._request(refresh_uri, body=body, method='POST', headers=headers)
129+
if response.code != 200:
126130
raise Error(response.read())
127131
response_args = simplejson.loads(response.read())
128132

129133
error = response_args.pop('error', None)
130134
if error is not None:
131135
raise Error(error)
132136

133-
return response_args['access_token'], response_args['refresh_token']
137+
self.access_token = response_args['access_token']
138+
self.refresh_token = response_args['refresh_token']
139+
return self.access_token, self.refresh_token
134140

135-
def refresh(self, uri, refresh_token, secret_type=None):
141+
def refresh_access_token(self, refresh_uri=None, refresh_token=None):
136142
""" Get a new access token from the supplied refresh token """
137143

144+
if refresh_uri is None:
145+
refresh_uri = self.refresh_uri
138146
if refresh_token is None:
139-
raise ValueError("Refresh_token must be set.")
147+
refresh_token = self.refresh_token
140148

141149
# prepare required args
142150
args = {
143-
'type': 'refresh',
144151
'client_id': self.client_id,
145152
'client_secret': self.client_secret,
146153
'refresh_token': refresh_token,
154+
'grant_type' : 'refresh_token',
147155
}
148-
149-
# prepare optional args
150-
if secret_type is not None:
151-
args['secret_type'] = secret_type
152-
153156
body = urllib.urlencode(args)
154-
headers = {
155-
'Content-Type': 'application/x-www-form-urlencoded',
156-
}
157157

158-
response = self.request(uri, method='POST', body=body, headers=headers)
159-
if not response.code == 200:
158+
headers = {'Content-type' : 'application/x-www-form-urlencoded'}
159+
if self.user_agent:
160+
headers['user-agent'] = self.user_agent
161+
162+
response = self._request(refresh_uri, method='POST', body=body, headers=headers)
163+
if response.code != 200:
160164
raise Error(response.read())
165+
response_args = simplejson.loads(response.read())
161166

162-
response_args = Client._split_url_string(content)
163-
return response_args
167+
self.access_token = response_args['access_token']
168+
# server may or may not supply a new refresh token
169+
self.refresh_token = response_args.get('refresh_token', self.refresh_token)
170+
return self.access_token, self.refresh_token
164171

165-
def request(self, uri, body=None, headers=None, method='GET'):
166-
if method == 'POST' and body is None:
172+
def _request(self, uri, body=None, headers=None, method='GET'):
173+
if method == 'POST' and not body:
167174
raise ValueError('POST requests must have a body')
175+
168176
request = urllib2.Request(uri, body, headers)
169177
return urllib2.urlopen(request, timeout=self.timeout)
178+
179+
def request(self, uri, body, headers, method='GET'):
180+
""" perform a HTTP request using OAuth authentication.
181+
If the request fails because the access token is expired it will
182+
try to refresh the token and try the request again.
183+
"""
184+
headers['Authorization'] = 'Bearer %s' % self.access_token
185+
186+
try:
187+
response = self._request(uri, body=body, headers=headers, method=method)
188+
except urllib2.HTTPError as e:
189+
if 400 <= e.code < 500:
190+
# any 400 code is acceptable to signal that the access token is expired.
191+
self.refresh_access_token()
192+
headers['Authorization'] = 'Bearer %s' % self.access_token
193+
response = self._request(uri, body=body, headers=headers, method=method)
194+
195+
if response.code == 200:
196+
return simplejson.loads(response.read())
197+
raise ValueError(response.read())
198+
199+
class GooglAPI(Client):
200+
user_agent = 'python-foauth2'
201+
# OAuth API
202+
auth_uri = 'https://accounts.google.com/o/oauth2/auth'
203+
refresh_uri = 'https://accounts.google.com/o/oauth2/token'
204+
scope = 'https://www.googleapis.com/auth/urlshortener'
205+
# data API
206+
api_uri = 'https://www.googleapis.com/urlshortener/v1/url'
207+
208+
def shorten(self, long_url):
209+
data = simplejson.dumps({'longUrl' : long_url})
210+
headers = {'Content-Type': 'application/json'}
211+
json_d = self.request(self.api_uri, data, headers, 'POST')
212+
return json_d['id']
213+
214+
def stats(self, short_url):
215+
params = {'shortUrl': short_url,
216+
'projection' : 'ANALYTICS_CLICKS',
217+
}
218+
stat_url = self.api_uri + '&' + urllib.urlencode(params)
219+
headers = {'Content-Type': 'application/json'}
220+
return self.request(stat_url, None, headers)

0 commit comments

Comments
 (0)