Revision 1a7c659b
/dev/null | ||
---|---|---|
1 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
import datetime |
|
35 |
|
|
36 |
from django.http import HttpResponse, HttpResponseBadRequest |
|
37 |
from django.utils.http import urlencode |
|
38 |
#from django.utils.cache import patch_vary_headers |
|
39 |
|
|
40 |
from models import User |
|
41 |
|
|
42 |
|
|
43 |
class Tokens: |
|
44 |
# these are mapped by the Shibboleth SP software |
|
45 |
SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName |
|
46 |
SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME" |
|
47 |
SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME" |
|
48 |
SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME" |
|
49 |
SHIB_DISPLAYNAME = "HTTP_SHIB_INETORGPERSON_DISPLAYNAME" |
|
50 |
SHIB_EP_AFFILIATION = "HTTP_SHIB_EP_AFFILIATION" |
|
51 |
SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID" |
|
52 |
|
|
53 |
|
|
54 |
def shibboleth(request): |
|
55 |
"""Register a user into the internal database |
|
56 |
and issue a token for subsequent requests. |
|
57 |
Users are authenticated by Shibboleth. |
|
58 |
|
|
59 |
Return the unique username and the token |
|
60 |
as 'X-Auth-User' and 'X-Auth-Token' headers, |
|
61 |
or redirect to the URL provided in 'next' |
|
62 |
with the 'user' and 'token' as parameters. |
|
63 |
|
|
64 |
Reissue the token even if it has not yet |
|
65 |
expired, if the 'renew' parameter is present. |
|
66 |
""" |
|
67 |
|
|
68 |
try: |
|
69 |
user = User.objects.get(uniq=request.META[Tokens.SHIB_EPPN]) |
|
70 |
except: |
|
71 |
user = None |
|
72 |
if user is None: |
|
73 |
tokens = request.META |
|
74 |
|
|
75 |
try: |
|
76 |
eppn = tokens[Tokens.SHIB_EPPN] |
|
77 |
except KeyError: |
|
78 |
return HttpResponseBadRequest("Missing unique token in request") |
|
79 |
|
|
80 |
if Tokens.SHIB_DISPLAYNAME in tokens: |
|
81 |
realname = tokens[Tokens.SHIB_DISPLAYNAME] |
|
82 |
elif Tokens.SHIB_CN in tokens: |
|
83 |
realname = tokens[Tokens.SHIB_CN] |
|
84 |
elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens: |
|
85 |
realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME] |
|
86 |
else: |
|
87 |
return HttpResponseBadRequest("Missing user name in request") |
|
88 |
|
|
89 |
user = User() |
|
90 |
user.uniq = eppn |
|
91 |
user.realname = realname |
|
92 |
user.affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '') |
|
93 |
user.renew_token() |
|
94 |
user.save() |
|
95 |
|
|
96 |
if 'renew' in request.GET or user.auth_token_expires < datetime.datetime.now(): |
|
97 |
user.renew_token() |
|
98 |
user.save() |
|
99 |
next = request.GET.get('next') |
|
100 |
if next is not None: |
|
101 |
# TODO: Avoid redirect loops. |
|
102 |
if '?' in next: |
|
103 |
next = next[:next.find('?')] |
|
104 |
next += '?' + urlencode({'user': user.uniq, |
|
105 |
'token': user.auth_token}) |
|
106 |
|
|
107 |
response = HttpResponse() |
|
108 |
# TODO: Cookie should only be set at the client side... |
|
109 |
#expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z') |
|
110 |
#response.set_cookie('X-Auth-Token', value=user.auth_token, expires=expire_fmt, path='/') |
|
111 |
if not next: |
|
112 |
response['X-Auth-User'] = user.uniq |
|
113 |
response['X-Auth-Token'] = user.auth_token |
|
114 |
response.content = user.uniq + '\n' + user.auth_token + '\n' |
|
115 |
response.status_code = 200 |
|
116 |
else: |
|
117 |
response['Location'] = next |
|
118 |
response.status_code = 302 |
|
119 |
return response |
b/pithos/im/oauth2/__init__.py | ||
---|---|---|
1 |
""" |
|
2 |
The MIT License |
|
3 |
|
|
4 |
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel |
|
5 |
|
|
6 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 |
of this software and associated documentation files (the "Software"), to deal |
|
8 |
in the Software without restriction, including without limitation the rights |
|
9 |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 |
copies of the Software, and to permit persons to whom the Software is |
|
11 |
furnished to do so, subject to the following conditions: |
|
12 |
|
|
13 |
The above copyright notice and this permission notice shall be included in |
|
14 |
all copies or substantial portions of the Software. |
|
15 |
|
|
16 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
22 |
THE SOFTWARE. |
|
23 |
""" |
|
24 |
|
|
25 |
import base64 |
|
26 |
import urllib |
|
27 |
import time |
|
28 |
import random |
|
29 |
import urlparse |
|
30 |
import hmac |
|
31 |
import binascii |
|
32 |
import httplib2 |
|
33 |
|
|
34 |
try: |
|
35 |
from urlparse import parse_qs |
|
36 |
parse_qs # placate pyflakes |
|
37 |
except ImportError: |
|
38 |
# fall back for Python 2.5 |
|
39 |
from cgi import parse_qs |
|
40 |
|
|
41 |
try: |
|
42 |
from hashlib import sha1 |
|
43 |
sha = sha1 |
|
44 |
except ImportError: |
|
45 |
# hashlib was added in Python 2.5 |
|
46 |
import sha |
|
47 |
|
|
48 |
import _version |
|
49 |
|
|
50 |
__version__ = _version.__version__ |
|
51 |
|
|
52 |
OAUTH_VERSION = '1.0' # Hi Blaine! |
|
53 |
HTTP_METHOD = 'GET' |
|
54 |
SIGNATURE_METHOD = 'PLAINTEXT' |
|
55 |
|
|
56 |
|
|
57 |
class Error(RuntimeError): |
|
58 |
"""Generic exception class.""" |
|
59 |
|
|
60 |
def __init__(self, message='OAuth error occurred.'): |
|
61 |
self._message = message |
|
62 |
|
|
63 |
@property |
|
64 |
def message(self): |
|
65 |
"""A hack to get around the deprecation errors in 2.6.""" |
|
66 |
return self._message |
|
67 |
|
|
68 |
def __str__(self): |
|
69 |
return self._message |
|
70 |
|
|
71 |
|
|
72 |
class MissingSignature(Error): |
|
73 |
pass |
|
74 |
|
|
75 |
|
|
76 |
def build_authenticate_header(realm=''): |
|
77 |
"""Optional WWW-Authenticate header (401 error)""" |
|
78 |
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
|
79 |
|
|
80 |
|
|
81 |
def build_xoauth_string(url, consumer, token=None): |
|
82 |
"""Build an XOAUTH string for use in SMTP/IMPA authentication.""" |
|
83 |
request = Request.from_consumer_and_token(consumer, token, |
|
84 |
"GET", url) |
|
85 |
|
|
86 |
signing_method = SignatureMethod_HMAC_SHA1() |
|
87 |
request.sign_request(signing_method, consumer, token) |
|
88 |
|
|
89 |
params = [] |
|
90 |
for k, v in sorted(request.iteritems()): |
|
91 |
if v is not None: |
|
92 |
params.append('%s="%s"' % (k, escape(v))) |
|
93 |
|
|
94 |
return "%s %s %s" % ("GET", url, ','.join(params)) |
|
95 |
|
|
96 |
|
|
97 |
def to_unicode(s): |
|
98 |
""" Convert to unicode, raise exception with instructive error |
|
99 |
message if s is not unicode, ascii, or utf-8. """ |
|
100 |
if not isinstance(s, unicode): |
|
101 |
if not isinstance(s, str): |
|
102 |
raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s)) |
|
103 |
try: |
|
104 |
s = s.decode('utf-8') |
|
105 |
except UnicodeDecodeError, le: |
|
106 |
raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,)) |
|
107 |
return s |
|
108 |
|
|
109 |
def to_utf8(s): |
|
110 |
return to_unicode(s).encode('utf-8') |
|
111 |
|
|
112 |
def to_unicode_if_string(s): |
|
113 |
if isinstance(s, basestring): |
|
114 |
return to_unicode(s) |
|
115 |
else: |
|
116 |
return s |
|
117 |
|
|
118 |
def to_utf8_if_string(s): |
|
119 |
if isinstance(s, basestring): |
|
120 |
return to_utf8(s) |
|
121 |
else: |
|
122 |
return s |
|
123 |
|
|
124 |
def to_unicode_optional_iterator(x): |
|
125 |
""" |
|
126 |
Raise TypeError if x is a str containing non-utf8 bytes or if x is |
|
127 |
an iterable which contains such a str. |
|
128 |
""" |
|
129 |
if isinstance(x, basestring): |
|
130 |
return to_unicode(x) |
|
131 |
|
|
132 |
try: |
|
133 |
l = list(x) |
|
134 |
except TypeError, e: |
|
135 |
assert 'is not iterable' in str(e) |
|
136 |
return x |
|
137 |
else: |
|
138 |
return [ to_unicode(e) for e in l ] |
|
139 |
|
|
140 |
def to_utf8_optional_iterator(x): |
|
141 |
""" |
|
142 |
Raise TypeError if x is a str or if x is an iterable which |
|
143 |
contains a str. |
|
144 |
""" |
|
145 |
if isinstance(x, basestring): |
|
146 |
return to_utf8(x) |
|
147 |
|
|
148 |
try: |
|
149 |
l = list(x) |
|
150 |
except TypeError, e: |
|
151 |
assert 'is not iterable' in str(e) |
|
152 |
return x |
|
153 |
else: |
|
154 |
return [ to_utf8_if_string(e) for e in l ] |
|
155 |
|
|
156 |
def escape(s): |
|
157 |
"""Escape a URL including any /.""" |
|
158 |
return urllib.quote(s.encode('utf-8'), safe='~') |
|
159 |
|
|
160 |
def generate_timestamp(): |
|
161 |
"""Get seconds since epoch (UTC).""" |
|
162 |
return int(time.time()) |
|
163 |
|
|
164 |
|
|
165 |
def generate_nonce(length=8): |
|
166 |
"""Generate pseudorandom number.""" |
|
167 |
return ''.join([str(random.randint(0, 9)) for i in range(length)]) |
|
168 |
|
|
169 |
|
|
170 |
def generate_verifier(length=8): |
|
171 |
"""Generate pseudorandom number.""" |
|
172 |
return ''.join([str(random.randint(0, 9)) for i in range(length)]) |
|
173 |
|
|
174 |
|
|
175 |
class Consumer(object): |
|
176 |
"""A consumer of OAuth-protected services. |
|
177 |
|
|
178 |
The OAuth consumer is a "third-party" service that wants to access |
|
179 |
protected resources from an OAuth service provider on behalf of an end |
|
180 |
user. It's kind of the OAuth client. |
|
181 |
|
|
182 |
Usually a consumer must be registered with the service provider by the |
|
183 |
developer of the consumer software. As part of that process, the service |
|
184 |
provider gives the consumer a *key* and a *secret* with which the consumer |
|
185 |
software can identify itself to the service. The consumer will include its |
|
186 |
key in each request to identify itself, but will use its secret only when |
|
187 |
signing requests, to prove that the request is from that particular |
|
188 |
registered consumer. |
|
189 |
|
|
190 |
Once registered, the consumer can then use its consumer credentials to ask |
|
191 |
the service provider for a request token, kicking off the OAuth |
|
192 |
authorization process. |
|
193 |
""" |
|
194 |
|
|
195 |
key = None |
|
196 |
secret = None |
|
197 |
|
|
198 |
def __init__(self, key, secret): |
|
199 |
self.key = key |
|
200 |
self.secret = secret |
|
201 |
|
|
202 |
if self.key is None or self.secret is None: |
|
203 |
raise ValueError("Key and secret must be set.") |
|
204 |
|
|
205 |
def __str__(self): |
|
206 |
data = {'oauth_consumer_key': self.key, |
|
207 |
'oauth_consumer_secret': self.secret} |
|
208 |
|
|
209 |
return urllib.urlencode(data) |
|
210 |
|
|
211 |
|
|
212 |
class Token(object): |
|
213 |
"""An OAuth credential used to request authorization or a protected |
|
214 |
resource. |
|
215 |
|
|
216 |
Tokens in OAuth comprise a *key* and a *secret*. The key is included in |
|
217 |
requests to identify the token being used, but the secret is used only in |
|
218 |
the signature, to prove that the requester is who the server gave the |
|
219 |
token to. |
|
220 |
|
|
221 |
When first negotiating the authorization, the consumer asks for a *request |
|
222 |
token* that the live user authorizes with the service provider. The |
|
223 |
consumer then exchanges the request token for an *access token* that can |
|
224 |
be used to access protected resources. |
|
225 |
""" |
|
226 |
|
|
227 |
key = None |
|
228 |
secret = None |
|
229 |
callback = None |
|
230 |
callback_confirmed = None |
|
231 |
verifier = None |
|
232 |
|
|
233 |
def __init__(self, key, secret): |
|
234 |
self.key = key |
|
235 |
self.secret = secret |
|
236 |
|
|
237 |
if self.key is None or self.secret is None: |
|
238 |
raise ValueError("Key and secret must be set.") |
|
239 |
|
|
240 |
def set_callback(self, callback): |
|
241 |
self.callback = callback |
|
242 |
self.callback_confirmed = 'true' |
|
243 |
|
|
244 |
def set_verifier(self, verifier=None): |
|
245 |
if verifier is not None: |
|
246 |
self.verifier = verifier |
|
247 |
else: |
|
248 |
self.verifier = generate_verifier() |
|
249 |
|
|
250 |
def get_callback_url(self): |
|
251 |
if self.callback and self.verifier: |
|
252 |
# Append the oauth_verifier. |
|
253 |
parts = urlparse.urlparse(self.callback) |
|
254 |
scheme, netloc, path, params, query, fragment = parts[:6] |
|
255 |
if query: |
|
256 |
query = '%s&oauth_verifier=%s' % (query, self.verifier) |
|
257 |
else: |
|
258 |
query = 'oauth_verifier=%s' % self.verifier |
|
259 |
return urlparse.urlunparse((scheme, netloc, path, params, |
|
260 |
query, fragment)) |
|
261 |
return self.callback |
|
262 |
|
|
263 |
def to_string(self): |
|
264 |
"""Returns this token as a plain string, suitable for storage. |
|
265 |
|
|
266 |
The resulting string includes the token's secret, so you should never |
|
267 |
send or store this string where a third party can read it. |
|
268 |
""" |
|
269 |
|
|
270 |
data = { |
|
271 |
'oauth_token': self.key, |
|
272 |
'oauth_token_secret': self.secret, |
|
273 |
} |
|
274 |
|
|
275 |
if self.callback_confirmed is not None: |
|
276 |
data['oauth_callback_confirmed'] = self.callback_confirmed |
|
277 |
return urllib.urlencode(data) |
|
278 |
|
|
279 |
@staticmethod |
|
280 |
def from_string(s): |
|
281 |
"""Deserializes a token from a string like one returned by |
|
282 |
`to_string()`.""" |
|
283 |
|
|
284 |
if not len(s): |
|
285 |
raise ValueError("Invalid parameter string.") |
|
286 |
|
|
287 |
params = parse_qs(s, keep_blank_values=False) |
|
288 |
if not len(params): |
|
289 |
raise ValueError("Invalid parameter string.") |
|
290 |
|
|
291 |
try: |
|
292 |
key = params['oauth_token'][0] |
|
293 |
except Exception: |
|
294 |
raise ValueError("'oauth_token' not found in OAuth request.") |
|
295 |
|
|
296 |
try: |
|
297 |
secret = params['oauth_token_secret'][0] |
|
298 |
except Exception: |
|
299 |
raise ValueError("'oauth_token_secret' not found in " |
|
300 |
"OAuth request.") |
|
301 |
|
|
302 |
token = Token(key, secret) |
|
303 |
try: |
|
304 |
token.callback_confirmed = params['oauth_callback_confirmed'][0] |
|
305 |
except KeyError: |
|
306 |
pass # 1.0, no callback confirmed. |
|
307 |
return token |
|
308 |
|
|
309 |
def __str__(self): |
|
310 |
return self.to_string() |
|
311 |
|
|
312 |
|
|
313 |
def setter(attr): |
|
314 |
name = attr.__name__ |
|
315 |
|
|
316 |
def getter(self): |
|
317 |
try: |
|
318 |
return self.__dict__[name] |
|
319 |
except KeyError: |
|
320 |
raise AttributeError(name) |
|
321 |
|
|
322 |
def deleter(self): |
|
323 |
del self.__dict__[name] |
|
324 |
|
|
325 |
return property(getter, attr, deleter) |
|
326 |
|
|
327 |
|
|
328 |
class Request(dict): |
|
329 |
|
|
330 |
"""The parameters and information for an HTTP request, suitable for |
|
331 |
authorizing with OAuth credentials. |
|
332 |
|
|
333 |
When a consumer wants to access a service's protected resources, it does |
|
334 |
so using a signed HTTP request identifying itself (the consumer) with its |
|
335 |
key, and providing an access token authorized by the end user to access |
|
336 |
those resources. |
|
337 |
|
|
338 |
""" |
|
339 |
|
|
340 |
version = OAUTH_VERSION |
|
341 |
|
|
342 |
def __init__(self, method=HTTP_METHOD, url=None, parameters=None, |
|
343 |
body='', is_form_encoded=False): |
|
344 |
if url is not None: |
|
345 |
self.url = to_unicode(url) |
|
346 |
self.method = method |
|
347 |
if parameters is not None: |
|
348 |
for k, v in parameters.iteritems(): |
|
349 |
k = to_unicode(k) |
|
350 |
v = to_unicode_optional_iterator(v) |
|
351 |
self[k] = v |
|
352 |
self.body = body |
|
353 |
self.is_form_encoded = is_form_encoded |
|
354 |
|
|
355 |
|
|
356 |
@setter |
|
357 |
def url(self, value): |
|
358 |
self.__dict__['url'] = value |
|
359 |
if value is not None: |
|
360 |
scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) |
|
361 |
|
|
362 |
# Exclude default port numbers. |
|
363 |
if scheme == 'http' and netloc[-3:] == ':80': |
|
364 |
netloc = netloc[:-3] |
|
365 |
elif scheme == 'https' and netloc[-4:] == ':443': |
|
366 |
netloc = netloc[:-4] |
|
367 |
if scheme not in ('http', 'https'): |
|
368 |
raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) |
|
369 |
|
|
370 |
# Normalized URL excludes params, query, and fragment. |
|
371 |
self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) |
|
372 |
else: |
|
373 |
self.normalized_url = None |
|
374 |
self.__dict__['url'] = None |
|
375 |
|
|
376 |
@setter |
|
377 |
def method(self, value): |
|
378 |
self.__dict__['method'] = value.upper() |
|
379 |
|
|
380 |
def _get_timestamp_nonce(self): |
|
381 |
return self['oauth_timestamp'], self['oauth_nonce'] |
|
382 |
|
|
383 |
def get_nonoauth_parameters(self): |
|
384 |
"""Get any non-OAuth parameters.""" |
|
385 |
return dict([(k, v) for k, v in self.iteritems() |
|
386 |
if not k.startswith('oauth_')]) |
|
387 |
|
|
388 |
def to_header(self, realm=''): |
|
389 |
"""Serialize as a header for an HTTPAuth request.""" |
|
390 |
oauth_params = ((k, v) for k, v in self.items() |
|
391 |
if k.startswith('oauth_')) |
|
392 |
stringy_params = ((k, escape(str(v))) for k, v in oauth_params) |
|
393 |
header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) |
|
394 |
params_header = ', '.join(header_params) |
|
395 |
|
|
396 |
auth_header = 'OAuth realm="%s"' % realm |
|
397 |
if params_header: |
|
398 |
auth_header = "%s, %s" % (auth_header, params_header) |
|
399 |
|
|
400 |
return {'Authorization': auth_header} |
|
401 |
|
|
402 |
def to_postdata(self): |
|
403 |
"""Serialize as post data for a POST request.""" |
|
404 |
d = {} |
|
405 |
for k, v in self.iteritems(): |
|
406 |
d[k.encode('utf-8')] = to_utf8_optional_iterator(v) |
|
407 |
|
|
408 |
# tell urlencode to deal with sequence values and map them correctly |
|
409 |
# to resulting querystring. for example self["k"] = ["v1", "v2"] will |
|
410 |
# result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D |
|
411 |
return urllib.urlencode(d, True).replace('+', '%20') |
|
412 |
|
|
413 |
def to_url(self): |
|
414 |
"""Serialize as a URL for a GET request.""" |
|
415 |
base_url = urlparse.urlparse(self.url) |
|
416 |
try: |
|
417 |
query = base_url.query |
|
418 |
except AttributeError: |
|
419 |
# must be python <2.5 |
|
420 |
query = base_url[4] |
|
421 |
query = parse_qs(query) |
|
422 |
for k, v in self.items(): |
|
423 |
query.setdefault(k, []).append(v) |
|
424 |
|
|
425 |
try: |
|
426 |
scheme = base_url.scheme |
|
427 |
netloc = base_url.netloc |
|
428 |
path = base_url.path |
|
429 |
params = base_url.params |
|
430 |
fragment = base_url.fragment |
|
431 |
except AttributeError: |
|
432 |
# must be python <2.5 |
|
433 |
scheme = base_url[0] |
|
434 |
netloc = base_url[1] |
|
435 |
path = base_url[2] |
|
436 |
params = base_url[3] |
|
437 |
fragment = base_url[5] |
|
438 |
|
|
439 |
url = (scheme, netloc, path, params, |
|
440 |
urllib.urlencode(query, True), fragment) |
|
441 |
return urlparse.urlunparse(url) |
|
442 |
|
|
443 |
def get_parameter(self, parameter): |
|
444 |
ret = self.get(parameter) |
|
445 |
if ret is None: |
|
446 |
raise Error('Parameter not found: %s' % parameter) |
|
447 |
|
|
448 |
return ret |
|
449 |
|
|
450 |
def get_normalized_parameters(self): |
|
451 |
"""Return a string that contains the parameters that must be signed.""" |
|
452 |
items = [] |
|
453 |
for key, value in self.iteritems(): |
|
454 |
if key == 'oauth_signature': |
|
455 |
continue |
|
456 |
# 1.0a/9.1.1 states that kvp must be sorted by key, then by value, |
|
457 |
# so we unpack sequence values into multiple items for sorting. |
|
458 |
if isinstance(value, basestring): |
|
459 |
items.append((to_utf8_if_string(key), to_utf8(value))) |
|
460 |
else: |
|
461 |
try: |
|
462 |
value = list(value) |
|
463 |
except TypeError, e: |
|
464 |
assert 'is not iterable' in str(e) |
|
465 |
items.append((to_utf8_if_string(key), to_utf8_if_string(value))) |
|
466 |
else: |
|
467 |
items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value) |
|
468 |
|
|
469 |
# Include any query string parameters from the provided URL |
|
470 |
query = urlparse.urlparse(self.url)[4] |
|
471 |
|
|
472 |
url_items = self._split_url_string(query).items() |
|
473 |
url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ] |
|
474 |
items.extend(url_items) |
|
475 |
|
|
476 |
items.sort() |
|
477 |
encoded_str = urllib.urlencode(items) |
|
478 |
# Encode signature parameters per Oauth Core 1.0 protocol |
|
479 |
# spec draft 7, section 3.6 |
|
480 |
# (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) |
|
481 |
# Spaces must be encoded with "%20" instead of "+" |
|
482 |
return encoded_str.replace('+', '%20').replace('%7E', '~') |
|
483 |
|
|
484 |
def sign_request(self, signature_method, consumer, token): |
|
485 |
"""Set the signature parameter to the result of sign.""" |
|
486 |
|
|
487 |
if not self.is_form_encoded: |
|
488 |
# according to |
|
489 |
# http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html |
|
490 |
# section 4.1.1 "OAuth Consumers MUST NOT include an |
|
491 |
# oauth_body_hash parameter on requests with form-encoded |
|
492 |
# request bodies." |
|
493 |
self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest()) |
|
494 |
|
|
495 |
if 'oauth_consumer_key' not in self: |
|
496 |
self['oauth_consumer_key'] = consumer.key |
|
497 |
|
|
498 |
if token and 'oauth_token' not in self: |
|
499 |
self['oauth_token'] = token.key |
|
500 |
|
|
501 |
self['oauth_signature_method'] = signature_method.name |
|
502 |
self['oauth_signature'] = signature_method.sign(self, consumer, token) |
|
503 |
|
|
504 |
@classmethod |
|
505 |
def make_timestamp(cls): |
|
506 |
"""Get seconds since epoch (UTC).""" |
|
507 |
return str(int(time.time())) |
|
508 |
|
|
509 |
@classmethod |
|
510 |
def make_nonce(cls): |
|
511 |
"""Generate pseudorandom number.""" |
|
512 |
return str(random.randint(0, 100000000)) |
|
513 |
|
|
514 |
@classmethod |
|
515 |
def from_request(cls, http_method, http_url, headers=None, parameters=None, |
|
516 |
query_string=None): |
|
517 |
"""Combines multiple parameter sources.""" |
|
518 |
if parameters is None: |
|
519 |
parameters = {} |
|
520 |
|
|
521 |
# Headers |
|
522 |
if headers and 'Authorization' in headers: |
|
523 |
auth_header = headers['Authorization'] |
|
524 |
# Check that the authorization header is OAuth. |
|
525 |
if auth_header[:6] == 'OAuth ': |
|
526 |
auth_header = auth_header[6:] |
|
527 |
try: |
|
528 |
# Get the parameters from the header. |
|
529 |
header_params = cls._split_header(auth_header) |
|
530 |
parameters.update(header_params) |
|
531 |
except: |
|
532 |
raise Error('Unable to parse OAuth parameters from ' |
|
533 |
'Authorization header.') |
|
534 |
|
|
535 |
# GET or POST query string. |
|
536 |
if query_string: |
|
537 |
query_params = cls._split_url_string(query_string) |
|
538 |
parameters.update(query_params) |
|
539 |
|
|
540 |
# URL parameters. |
|
541 |
param_str = urlparse.urlparse(http_url)[4] # query |
|
542 |
url_params = cls._split_url_string(param_str) |
|
543 |
parameters.update(url_params) |
|
544 |
|
|
545 |
if parameters: |
|
546 |
return cls(http_method, http_url, parameters) |
|
547 |
|
|
548 |
return None |
|
549 |
|
|
550 |
@classmethod |
|
551 |
def from_consumer_and_token(cls, consumer, token=None, |
|
552 |
http_method=HTTP_METHOD, http_url=None, parameters=None, |
|
553 |
body='', is_form_encoded=False): |
|
554 |
if not parameters: |
|
555 |
parameters = {} |
|
556 |
|
|
557 |
defaults = { |
|
558 |
'oauth_consumer_key': consumer.key, |
|
559 |
'oauth_timestamp': cls.make_timestamp(), |
|
560 |
'oauth_nonce': cls.make_nonce(), |
|
561 |
'oauth_version': cls.version, |
|
562 |
} |
|
563 |
|
|
564 |
defaults.update(parameters) |
|
565 |
parameters = defaults |
|
566 |
|
|
567 |
if token: |
|
568 |
parameters['oauth_token'] = token.key |
|
569 |
if token.verifier: |
|
570 |
parameters['oauth_verifier'] = token.verifier |
|
571 |
|
|
572 |
return Request(http_method, http_url, parameters, body=body, |
|
573 |
is_form_encoded=is_form_encoded) |
|
574 |
|
|
575 |
@classmethod |
|
576 |
def from_token_and_callback(cls, token, callback=None, |
|
577 |
http_method=HTTP_METHOD, http_url=None, parameters=None): |
|
578 |
|
|
579 |
if not parameters: |
|
580 |
parameters = {} |
|
581 |
|
|
582 |
parameters['oauth_token'] = token.key |
|
583 |
|
|
584 |
if callback: |
|
585 |
parameters['oauth_callback'] = callback |
|
586 |
|
|
587 |
return cls(http_method, http_url, parameters) |
|
588 |
|
|
589 |
@staticmethod |
|
590 |
def _split_header(header): |
|
591 |
"""Turn Authorization: header into parameters.""" |
|
592 |
params = {} |
|
593 |
parts = header.split(',') |
|
594 |
for param in parts: |
|
595 |
# Ignore realm parameter. |
|
596 |
if param.find('realm') > -1: |
|
597 |
continue |
|
598 |
# Remove whitespace. |
|
599 |
param = param.strip() |
|
600 |
# Split key-value. |
|
601 |
param_parts = param.split('=', 1) |
|
602 |
# Remove quotes and unescape the value. |
|
603 |
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) |
|
604 |
return params |
|
605 |
|
|
606 |
@staticmethod |
|
607 |
def _split_url_string(param_str): |
|
608 |
"""Turn URL string into parameters.""" |
|
609 |
parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True) |
|
610 |
for k, v in parameters.iteritems(): |
|
611 |
parameters[k] = urllib.unquote(v[0]) |
|
612 |
return parameters |
|
613 |
|
|
614 |
|
|
615 |
class Client(httplib2.Http): |
|
616 |
"""OAuthClient is a worker to attempt to execute a request.""" |
|
617 |
|
|
618 |
def __init__(self, consumer, token=None, cache=None, timeout=None, |
|
619 |
proxy_info=None): |
|
620 |
|
|
621 |
if consumer is not None and not isinstance(consumer, Consumer): |
|
622 |
raise ValueError("Invalid consumer.") |
|
623 |
|
|
624 |
if token is not None and not isinstance(token, Token): |
|
625 |
raise ValueError("Invalid token.") |
|
626 |
|
|
627 |
self.consumer = consumer |
|
628 |
self.token = token |
|
629 |
self.method = SignatureMethod_HMAC_SHA1() |
|
630 |
|
|
631 |
httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info) |
|
632 |
|
|
633 |
def set_signature_method(self, method): |
|
634 |
if not isinstance(method, SignatureMethod): |
|
635 |
raise ValueError("Invalid signature method.") |
|
636 |
|
|
637 |
self.method = method |
|
638 |
|
|
639 |
def request(self, uri, method="GET", body='', headers=None, |
|
640 |
redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): |
|
641 |
DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded' |
|
642 |
|
|
643 |
if not isinstance(headers, dict): |
|
644 |
headers = {} |
|
645 |
|
|
646 |
if method == "POST": |
|
647 |
headers['Content-Type'] = headers.get('Content-Type', |
|
648 |
DEFAULT_POST_CONTENT_TYPE) |
|
649 |
|
|
650 |
is_form_encoded = \ |
|
651 |
headers.get('Content-Type') == 'application/x-www-form-urlencoded' |
|
652 |
|
|
653 |
if is_form_encoded and body: |
|
654 |
parameters = parse_qs(body) |
|
655 |
else: |
|
656 |
parameters = None |
|
657 |
|
|
658 |
req = Request.from_consumer_and_token(self.consumer, |
|
659 |
token=self.token, http_method=method, http_url=uri, |
|
660 |
parameters=parameters, body=body, is_form_encoded=is_form_encoded) |
|
661 |
|
|
662 |
req.sign_request(self.method, self.consumer, self.token) |
|
663 |
|
|
664 |
schema, rest = urllib.splittype(uri) |
|
665 |
if rest.startswith('//'): |
|
666 |
hierpart = '//' |
|
667 |
else: |
|
668 |
hierpart = '' |
|
669 |
host, rest = urllib.splithost(rest) |
|
670 |
|
|
671 |
realm = schema + ':' + hierpart + host |
|
672 |
|
|
673 |
if is_form_encoded: |
|
674 |
body = req.to_postdata() |
|
675 |
elif method == "GET": |
|
676 |
uri = req.to_url() |
|
677 |
else: |
|
678 |
headers.update(req.to_header(realm=realm)) |
|
679 |
|
|
680 |
return httplib2.Http.request(self, uri, method=method, body=body, |
|
681 |
headers=headers, redirections=redirections, |
|
682 |
connection_type=connection_type) |
|
683 |
|
|
684 |
|
|
685 |
class Server(object): |
|
686 |
"""A skeletal implementation of a service provider, providing protected |
|
687 |
resources to requests from authorized consumers. |
|
688 |
|
|
689 |
This class implements the logic to check requests for authorization. You |
|
690 |
can use it with your web server or web framework to protect certain |
|
691 |
resources with OAuth. |
|
692 |
""" |
|
693 |
|
|
694 |
timestamp_threshold = 300 # In seconds, five minutes. |
|
695 |
version = OAUTH_VERSION |
|
696 |
signature_methods = None |
|
697 |
|
|
698 |
def __init__(self, signature_methods=None): |
|
699 |
self.signature_methods = signature_methods or {} |
|
700 |
|
|
701 |
def add_signature_method(self, signature_method): |
|
702 |
self.signature_methods[signature_method.name] = signature_method |
|
703 |
return self.signature_methods |
|
704 |
|
|
705 |
def verify_request(self, request, consumer, token): |
|
706 |
"""Verifies an api call and checks all the parameters.""" |
|
707 |
|
|
708 |
self._check_version(request) |
|
709 |
self._check_signature(request, consumer, token) |
|
710 |
parameters = request.get_nonoauth_parameters() |
|
711 |
return parameters |
|
712 |
|
|
713 |
def build_authenticate_header(self, realm=''): |
|
714 |
"""Optional support for the authenticate header.""" |
|
715 |
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
|
716 |
|
|
717 |
def _check_version(self, request): |
|
718 |
"""Verify the correct version of the request for this server.""" |
|
719 |
version = self._get_version(request) |
|
720 |
if version and version != self.version: |
|
721 |
raise Error('OAuth version %s not supported.' % str(version)) |
|
722 |
|
|
723 |
def _get_version(self, request): |
|
724 |
"""Return the version of the request for this server.""" |
|
725 |
try: |
|
726 |
version = request.get_parameter('oauth_version') |
|
727 |
except: |
|
728 |
version = OAUTH_VERSION |
|
729 |
|
|
730 |
return version |
|
731 |
|
|
732 |
def _get_signature_method(self, request): |
|
733 |
"""Figure out the signature with some defaults.""" |
|
734 |
try: |
|
735 |
signature_method = request.get_parameter('oauth_signature_method') |
|
736 |
except: |
|
737 |
signature_method = SIGNATURE_METHOD |
|
738 |
|
|
739 |
try: |
|
740 |
# Get the signature method object. |
|
741 |
signature_method = self.signature_methods[signature_method] |
|
742 |
except: |
|
743 |
signature_method_names = ', '.join(self.signature_methods.keys()) |
|
744 |
raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
|
745 |
|
|
746 |
return signature_method |
|
747 |
|
|
748 |
def _get_verifier(self, request): |
|
749 |
return request.get_parameter('oauth_verifier') |
|
750 |
|
|
751 |
def _check_signature(self, request, consumer, token): |
|
752 |
timestamp, nonce = request._get_timestamp_nonce() |
|
753 |
self._check_timestamp(timestamp) |
|
754 |
signature_method = self._get_signature_method(request) |
|
755 |
|
|
756 |
try: |
|
757 |
signature = request.get_parameter('oauth_signature') |
|
758 |
except: |
|
759 |
raise MissingSignature('Missing oauth_signature.') |
|
760 |
|
|
761 |
# Validate the signature. |
|
762 |
valid = signature_method.check(request, consumer, token, signature) |
|
763 |
|
|
764 |
if not valid: |
|
765 |
key, base = signature_method.signing_base(request, consumer, token) |
|
766 |
|
|
767 |
raise Error('Invalid signature. Expected signature base ' |
|
768 |
'string: %s' % base) |
|
769 |
|
|
770 |
def _check_timestamp(self, timestamp): |
|
771 |
"""Verify that timestamp is recentish.""" |
|
772 |
timestamp = int(timestamp) |
|
773 |
now = int(time.time()) |
|
774 |
lapsed = now - timestamp |
|
775 |
if lapsed > self.timestamp_threshold: |
|
776 |
raise Error('Expired timestamp: given %d and now %s has a ' |
|
777 |
'greater difference than threshold %d' % (timestamp, now, |
|
778 |
self.timestamp_threshold)) |
|
779 |
|
|
780 |
|
|
781 |
class SignatureMethod(object): |
|
782 |
"""A way of signing requests. |
|
783 |
|
|
784 |
The OAuth protocol lets consumers and service providers pick a way to sign |
|
785 |
requests. This interface shows the methods expected by the other `oauth` |
|
786 |
modules for signing requests. Subclass it and implement its methods to |
|
787 |
provide a new way to sign requests. |
|
788 |
""" |
|
789 |
|
|
790 |
def signing_base(self, request, consumer, token): |
|
791 |
"""Calculates the string that needs to be signed. |
|
792 |
|
|
793 |
This method returns a 2-tuple containing the starting key for the |
|
794 |
signing and the message to be signed. The latter may be used in error |
|
795 |
messages to help clients debug their software. |
|
796 |
|
|
797 |
""" |
|
798 |
raise NotImplementedError |
|
799 |
|
|
800 |
def sign(self, request, consumer, token): |
|
801 |
"""Returns the signature for the given request, based on the consumer |
|
802 |
and token also provided. |
|
803 |
|
|
804 |
You should use your implementation of `signing_base()` to build the |
|
805 |
message to sign. Otherwise it may be less useful for debugging. |
|
806 |
|
|
807 |
""" |
|
808 |
raise NotImplementedError |
|
809 |
|
|
810 |
def check(self, request, consumer, token, signature): |
|
811 |
"""Returns whether the given signature is the correct signature for |
|
812 |
the given consumer and token signing the given request.""" |
|
813 |
built = self.sign(request, consumer, token) |
|
814 |
return built == signature |
|
815 |
|
|
816 |
|
|
817 |
class SignatureMethod_HMAC_SHA1(SignatureMethod): |
|
818 |
name = 'HMAC-SHA1' |
|
819 |
|
|
820 |
def signing_base(self, request, consumer, token): |
|
821 |
if not hasattr(request, 'normalized_url') or request.normalized_url is None: |
|
822 |
raise ValueError("Base URL for request is not set.") |
|
823 |
|
|
824 |
sig = ( |
|
825 |
escape(request.method), |
|
826 |
escape(request.normalized_url), |
|
827 |
escape(request.get_normalized_parameters()), |
|
828 |
) |
|
829 |
|
|
830 |
key = '%s&' % escape(consumer.secret) |
|
831 |
if token: |
|
832 |
key += escape(token.secret) |
|
833 |
raw = '&'.join(sig) |
|
834 |
return key, raw |
|
835 |
|
|
836 |
def sign(self, request, consumer, token): |
|
837 |
"""Builds the base signature string.""" |
|
838 |
key, raw = self.signing_base(request, consumer, token) |
|
839 |
|
|
840 |
hashed = hmac.new(key, raw, sha) |
|
841 |
|
|
842 |
# Calculate the digest base 64. |
|
843 |
return binascii.b2a_base64(hashed.digest())[:-1] |
|
844 |
|
|
845 |
|
|
846 |
class SignatureMethod_PLAINTEXT(SignatureMethod): |
|
847 |
|
|
848 |
name = 'PLAINTEXT' |
|
849 |
|
|
850 |
def signing_base(self, request, consumer, token): |
|
851 |
"""Concatenates the consumer key and secret with the token's |
|
852 |
secret.""" |
|
853 |
sig = '%s&' % escape(consumer.secret) |
|
854 |
if token: |
|
855 |
sig = sig + escape(token.secret) |
|
856 |
return sig, sig |
|
857 |
|
|
858 |
def sign(self, request, consumer, token): |
|
859 |
key, raw = self.signing_base(request, consumer, token) |
|
860 |
return raw |
b/pithos/im/oauth2/_version.py | ||
---|---|---|
1 |
# This is the version of this source code. |
|
2 |
|
|
3 |
manual_verstr = "1.5" |
|
4 |
|
|
5 |
|
|
6 |
|
|
7 |
auto_build_num = "170" |
|
8 |
|
|
9 |
|
|
10 |
|
|
11 |
verstr = manual_verstr + "." + auto_build_num |
|
12 |
try: |
|
13 |
from pyutil.version_class import Version as pyutil_Version |
|
14 |
__version__ = pyutil_Version(verstr) |
|
15 |
except (ImportError, ValueError): |
|
16 |
# Maybe there is no pyutil installed. |
|
17 |
from distutils.version import LooseVersion as distutils_Version |
|
18 |
__version__ = distutils_Version(verstr) |
b/pithos/im/oauth2/clients/imap.py | ||
---|---|---|
1 |
""" |
|
2 |
The MIT License |
|
3 |
|
|
4 |
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel |
|
5 |
|
|
6 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 |
of this software and associated documentation files (the "Software"), to deal |
|
8 |
in the Software without restriction, including without limitation the rights |
|
9 |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 |
copies of the Software, and to permit persons to whom the Software is |
|
11 |
furnished to do so, subject to the following conditions: |
|
12 |
|
|
13 |
The above copyright notice and this permission notice shall be included in |
|
14 |
all copies or substantial portions of the Software. |
|
15 |
|
|
16 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
22 |
THE SOFTWARE. |
|
23 |
""" |
|
24 |
|
|
25 |
import oauth2 |
|
26 |
import imaplib |
|
27 |
|
|
28 |
|
|
29 |
class IMAP4_SSL(imaplib.IMAP4_SSL): |
|
30 |
"""IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH.""" |
|
31 |
|
|
32 |
def authenticate(self, url, consumer, token): |
|
33 |
if consumer is not None and not isinstance(consumer, oauth2.Consumer): |
|
34 |
raise ValueError("Invalid consumer.") |
|
35 |
|
|
36 |
if token is not None and not isinstance(token, oauth2.Token): |
|
37 |
raise ValueError("Invalid token.") |
|
38 |
|
|
39 |
imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH', |
|
40 |
lambda x: oauth2.build_xoauth_string(url, consumer, token)) |
b/pithos/im/oauth2/clients/smtp.py | ||
---|---|---|
1 |
""" |
|
2 |
The MIT License |
|
3 |
|
|
4 |
Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel |
|
5 |
|
|
6 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 |
of this software and associated documentation files (the "Software"), to deal |
|
8 |
in the Software without restriction, including without limitation the rights |
|
9 |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 |
copies of the Software, and to permit persons to whom the Software is |
|
11 |
furnished to do so, subject to the following conditions: |
|
12 |
|
|
13 |
The above copyright notice and this permission notice shall be included in |
|
14 |
all copies or substantial portions of the Software. |
|
15 |
|
|
16 |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
22 |
THE SOFTWARE. |
|
23 |
""" |
|
24 |
|
|
25 |
import oauth2 |
|
26 |
import smtplib |
|
27 |
import base64 |
|
28 |
|
|
29 |
|
|
30 |
class SMTP(smtplib.SMTP): |
|
31 |
"""SMTP wrapper for smtplib.SMTP that implements XOAUTH.""" |
|
32 |
|
|
33 |
def authenticate(self, url, consumer, token): |
|
34 |
if consumer is not None and not isinstance(consumer, oauth2.Consumer): |
|
35 |
raise ValueError("Invalid consumer.") |
|
36 |
|
|
37 |
if token is not None and not isinstance(token, oauth2.Token): |
|
38 |
raise ValueError("Invalid token.") |
|
39 |
|
|
40 |
self.docmd('AUTH', 'XOAUTH %s' % \ |
|
41 |
base64.b64encode(oauth2.build_xoauth_string(url, consumer, token))) |
b/pithos/im/shibboleth.py | ||
---|---|---|
1 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
import datetime |
|
35 |
|
|
36 |
from urlparse import urlsplit, urlunsplit |
|
37 |
|
|
38 |
from django.http import HttpResponse, HttpResponseBadRequest |
|
39 |
from django.utils.http import urlencode |
|
40 |
#from django.utils.cache import patch_vary_headers |
|
41 |
|
|
42 |
from models import User |
|
43 |
|
|
44 |
|
|
45 |
class Tokens: |
|
46 |
# these are mapped by the Shibboleth SP software |
|
47 |
SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName |
|
48 |
SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME" |
|
49 |
SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME" |
|
50 |
SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME" |
|
51 |
SHIB_DISPLAYNAME = "HTTP_SHIB_INETORGPERSON_DISPLAYNAME" |
|
52 |
SHIB_EP_AFFILIATION = "HTTP_SHIB_EP_AFFILIATION" |
|
53 |
SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID" |
|
54 |
|
|
55 |
|
|
56 |
def login(request): |
|
57 |
"""Register a user into the internal database |
|
58 |
and issue a token for subsequent requests. |
|
59 |
Users are authenticated by Shibboleth. |
|
60 |
|
|
61 |
Return the unique username and the token |
|
62 |
as 'X-Auth-User' and 'X-Auth-Token' headers, |
|
63 |
or redirect to the URL provided in 'next' |
|
64 |
with the 'user' and 'token' as parameters. |
|
65 |
|
|
66 |
Reissue the token even if it has not yet |
|
67 |
expired, if the 'renew' parameter is present. |
|
68 |
""" |
|
69 |
|
|
70 |
try: |
|
71 |
user = User.objects.get(uniq=request.META[Tokens.SHIB_EPPN]) |
|
72 |
except: |
|
73 |
user = None |
|
74 |
if user is None: |
|
75 |
tokens = request.META |
|
76 |
|
|
77 |
try: |
|
78 |
eppn = tokens[Tokens.SHIB_EPPN] |
|
79 |
except KeyError: |
|
80 |
return HttpResponseBadRequest("Missing unique token in request") |
|
81 |
|
|
82 |
if Tokens.SHIB_DISPLAYNAME in tokens: |
|
83 |
realname = tokens[Tokens.SHIB_DISPLAYNAME] |
|
84 |
elif Tokens.SHIB_CN in tokens: |
|
85 |
realname = tokens[Tokens.SHIB_CN] |
|
86 |
elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens: |
|
87 |
realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME] |
|
88 |
else: |
|
89 |
return HttpResponseBadRequest("Missing user name in request") |
|
90 |
|
|
91 |
user = User() |
|
92 |
user.uniq = eppn |
|
93 |
user.realname = realname |
|
94 |
user.affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '') |
|
95 |
user.renew_token() |
|
96 |
user.save() |
|
97 |
|
|
98 |
if 'renew' in request.GET or user.auth_token_expires < datetime.datetime.now(): |
|
99 |
user.renew_token() |
|
100 |
user.save() |
|
101 |
next = request.GET.get('next') |
|
102 |
if next is not None: |
|
103 |
# TODO: Avoid redirect loops. |
|
104 |
parts = list(urlsplit(next)) |
|
105 |
parts[3] = urlencode({'user': user.uniq, 'token': user.auth_token}) |
|
106 |
next = urlunsplit(parts) |
|
107 |
|
|
108 |
response = HttpResponse() |
|
109 |
# TODO: Cookie should only be set at the client side... |
|
110 |
#expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z') |
|
111 |
#response.set_cookie('X-Auth-Token', value=user.auth_token, expires=expire_fmt, path='/') |
|
112 |
if not next: |
|
113 |
response['X-Auth-User'] = user.uniq |
|
114 |
response['X-Auth-Token'] = user.auth_token |
|
115 |
response.content = user.uniq + '\n' + user.auth_token + '\n' |
|
116 |
response.status_code = 200 |
|
117 |
else: |
|
118 |
response['Location'] = next |
|
119 |
response.status_code = 302 |
|
120 |
return response |
b/pithos/im/templates/index.html | ||
---|---|---|
10 | 10 |
</head> |
11 | 11 |
<body> |
12 | 12 |
<div class="container"> |
13 |
<div style="padding: 5px 0px 0px 0px"> |
|
14 |
<img src="/im/static/banner.png" width="900" height="200"> |
|
15 |
</div> |
|
13 | 16 |
<h2>Welcome</h2> |
14 |
<p>Choose how to login!</p>
|
|
17 |
<p>Choose how to login. Or move on to <a href="admin">admin</a>.</p>
|
|
15 | 18 |
<div class="row"> |
16 | 19 |
<div class="span4"> |
17 | 20 |
<h4>Traditional</h4> |
b/pithos/im/twitter.py | ||
---|---|---|
1 |
# Copyright 2011 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
# This is based on the docs at: https://github.com/simplegeo/python-oauth2 |
|
35 |
|
|
36 |
import oauth2 as oauth |
|
37 |
import urlparse |
|
38 |
|
|
39 |
from django.conf import settings |
|
40 |
from django.http import HttpResponse, HttpResponseRedirect |
|
41 |
|
|
42 |
from models import User |
|
43 |
|
|
44 |
# It's probably a good idea to put your consumer's OAuth token and |
|
45 |
# OAuth secret into your project's settings. |
|
46 |
consumer = oauth.Consumer(settings.TWITTER_KEY, settings.TWITTER_SECRET) |
|
47 |
client = oauth.Client(consumer) |
|
48 |
|
|
49 |
request_token_url = 'http://twitter.com/oauth/request_token' |
|
50 |
access_token_url = 'http://twitter.com/oauth/access_token' |
|
51 |
|
|
52 |
# This is the slightly different URL used to authenticate/authorize. |
|
53 |
authenticate_url = 'http://twitter.com/oauth/authenticate' |
|
54 |
|
|
55 |
def login(request): |
|
56 |
# Step 1. Get a request token from Twitter. |
|
57 |
resp, content = client.request(request_token_url, "GET") |
|
58 |
if resp['status'] != '200': |
|
59 |
raise Exception("Invalid response from Twitter.") |
|
60 |
|
|
61 |
# Step 2. Store the request token in a session for later use. |
|
62 |
response = HttpResponse() |
|
63 |
response.set_cookie('Twitter-Request-Token', value=content, max_age=300) |
|
64 |
|
|
65 |
# Step 3. Redirect the user to the authentication URL. |
|
66 |
request_token = dict(urlparse.parse_qsl(content)) |
|
67 |
url = "%s?oauth_token=%s" % (authenticate_url, |
|
68 |
request_token['oauth_token']) |
|
69 |
response['Location'] = url |
|
70 |
response.status_code = 302 |
|
71 |
|
|
72 |
return response |
|
73 |
|
|
74 |
def authenticated(request): |
|
75 |
# Step 1. Use the request token in the session to build a new client. |
|
76 |
content = request.COOKIES.get('Twitter-Request-Token', None) |
|
77 |
if not content: |
|
78 |
raise Exception("Request token cookie not found.") |
|
79 |
request_token = dict(urlparse.parse_qsl(content)) |
|
80 |
token = oauth.Token(request_token['oauth_token'], |
|
81 |
request_token['oauth_token_secret']) |
|
82 |
client = oauth.Client(consumer, token) |
|
83 |
|
|
84 |
# Step 2. Request the authorized access token from Twitter. |
|
85 |
resp, content = client.request(access_token_url, "GET") |
|
86 |
if resp['status'] != '200': |
|
87 |
raise Exception("Invalid response from Twitter.") |
|
88 |
|
|
89 |
""" |
|
90 |
This is what you'll get back from Twitter. Note that it includes the |
|
91 |
user's user_id and screen_name. |
|
92 |
{ |
|
93 |
'oauth_token_secret': 'IcJXPiJh8be3BjDWW50uCY31chyhsMHEhqJVsphC3M', |
|
94 |
'user_id': '120889797', |
|
95 |
'oauth_token': '120889797-H5zNnM3qE0iFoTTpNEHIz3noL9FKzXiOxwtnyVOD', |
|
96 |
'screen_name': 'heyismysiteup' |
|
97 |
} |
|
98 |
""" |
|
99 |
access_token = dict(urlparse.parse_qsl(content)) |
|
100 |
|
|
101 |
# Step 3. Lookup the user or create them if they don't exist. |
|
102 |
try: |
|
103 |
user = User.objects.get(uniq=access_token['screen_name']) |
|
104 |
except User.DoesNotExist: |
|
105 |
# When creating the user I just use their screen_name@twitter.com |
|
106 |
# for their email and the oauth_token_secret for their password. |
|
107 |
# These two things will likely never be used. Alternatively, you |
|
108 |
# can prompt them for their email here. Either way, the password |
|
109 |
# should never be used. |
|
110 |
user = User() |
|
111 |
user.uniq = '%s@twitter.com' % access_token['screen_name'] |
|
112 |
user.realname = access_token['oauth_token'] |
|
113 |
user.affiliation = 'Twitter' |
|
114 |
user.renew_token() |
|
115 |
user.auth_token = access_token['oauth_token_secret'] |
|
116 |
user.save() |
|
117 |
|
|
118 |
response = HttpResponse() |
|
119 |
response.content = user.uniq + '\n' + user.auth_token + '\n' |
|
120 |
response.status_code = 200 |
|
121 |
return response |
b/pithos/im/urls.py | ||
---|---|---|
48 | 48 |
(r'^admin/users/(\d+)/delete/?$', 'users_delete') |
49 | 49 |
) |
50 | 50 |
|
51 |
urlpatterns += patterns('pithos.im.login', |
|
52 |
(r'^login/shibboleth/?$', 'shibboleth') |
|
51 |
urlpatterns += patterns('', |
|
52 |
(r'^login/shibboleth/?$', 'pithos.im.shibboleth.login'), |
|
53 |
(r'^login/twitter/?$', 'pithos.im.twitter.login'), |
|
54 |
(r'^login/twitter/authenticated/?$', 'pithos.im.twitter.authenticated') |
|
53 | 55 |
) |
54 | 56 |
|
55 | 57 |
urlpatterns += patterns('', |
b/pithos/settings.py.dist | ||
---|---|---|
162 | 162 |
|
163 | 163 |
# Show these many users per page in admin interface. |
164 | 164 |
ADMIN_PAGE_LIMIT = 100 |
165 |
|
|
166 |
# Authenticate via Twitter. |
|
167 |
TWITTER_TOKEN = '' |
|
168 |
TWITTER_SECRET = '' |
Also available in: Unified diff