Merge branch 'master' of http://github.com/fnp/wolnelektury
[wolnelektury.git] / apps / piston / authentication.py
1 import binascii
2
3 import oauth
4 from django.http import HttpResponse, HttpResponseRedirect
5 from django.contrib.auth.models import User, AnonymousUser
6 from django.contrib.auth.decorators import login_required
7 from django.template import loader
8 from django.contrib.auth import authenticate
9 from django.conf import settings
10 from django.core.urlresolvers import get_callable
11 from django.core.exceptions import ImproperlyConfigured
12 from django.shortcuts import render_to_response
13 from django.template import RequestContext
14
15 from piston import forms
16
17 class NoAuthentication(object):
18     """
19     Authentication handler that always returns
20     True, so no authentication is needed, nor
21     initiated (`challenge` is missing.)
22     """
23     def is_authenticated(self, request):
24         return True
25
26 class HttpBasicAuthentication(object):
27     """
28     Basic HTTP authenticater. Synopsis:
29     
30     Authentication handlers must implement two methods:
31      - `is_authenticated`: Will be called when checking for
32         authentication. Receives a `request` object, please
33         set your `User` object on `request.user`, otherwise
34         return False (or something that evaluates to False.)
35      - `challenge`: In cases where `is_authenticated` returns
36         False, the result of this method will be returned.
37         This will usually be a `HttpResponse` object with
38         some kind of challenge headers and 401 code on it.
39     """
40     def __init__(self, auth_func=authenticate, realm='API'):
41         self.auth_func = auth_func
42         self.realm = realm
43
44     def is_authenticated(self, request):
45         auth_string = request.META.get('HTTP_AUTHORIZATION', None)
46
47         if not auth_string:
48             return False
49             
50         try:
51             (authmeth, auth) = auth_string.split(" ", 1)
52
53             if not authmeth.lower() == 'basic':
54                 return False
55
56             auth = auth.strip().decode('base64')
57             (username, password) = auth.split(':', 1)
58         except (ValueError, binascii.Error):
59             return False
60         
61         request.user = self.auth_func(username=username, password=password) \
62             or AnonymousUser()
63                 
64         return not request.user in (False, None, AnonymousUser())
65         
66     def challenge(self):
67         resp = HttpResponse("Authorization Required")
68         resp['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm
69         resp.status_code = 401
70         return resp
71
72     def __repr__(self):
73         return u'<HTTPBasic: realm=%s>' % self.realm
74
75 class HttpBasicSimple(HttpBasicAuthentication):
76     def __init__(self, realm, username, password):
77         self.user = User.objects.get(username=username)
78         self.password = password
79
80         super(HttpBasicSimple, self).__init__(auth_func=self.hash, realm=realm)
81     
82     def hash(self, username, password):
83         if username == self.user.username and password == self.password:
84             return self.user
85
86 def load_data_store():
87     '''Load data store for OAuth Consumers, Tokens, Nonces and Resources
88     '''
89     path = getattr(settings, 'OAUTH_DATA_STORE', 'piston.store.DataStore')
90
91     # stolen from django.contrib.auth.load_backend
92     i = path.rfind('.')
93     module, attr = path[:i], path[i+1:]
94
95     try:
96         mod = __import__(module, {}, {}, attr)
97     except ImportError, e:
98         raise ImproperlyConfigured, 'Error importing OAuth data store %s: "%s"' % (module, e)
99
100     try:
101         cls = getattr(mod, attr)
102     except AttributeError:
103         raise ImproperlyConfigured, 'Module %s does not define a "%s" OAuth data store' % (module, attr)
104
105     return cls
106
107 # Set the datastore here.
108 oauth_datastore = load_data_store()
109
110 def initialize_server_request(request):
111     """
112     Shortcut for initialization.
113     """
114     if request.method == "POST": #and \
115 #       request.META['CONTENT_TYPE'] == "application/x-www-form-urlencoded":
116         params = dict(request.REQUEST.items())
117     else:
118         params = { }
119
120     # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
121     # for oauth.py to understand. Lovely.
122     request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
123
124     oauth_request = oauth.OAuthRequest.from_request(
125         request.method, request.build_absolute_uri(), 
126         headers=request.META, parameters=params,
127         query_string=request.environ.get('QUERY_STRING', ''))
128         
129     if oauth_request:
130         oauth_server = oauth.OAuthServer(oauth_datastore(oauth_request))
131         oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
132         oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
133     else:
134         oauth_server = None
135         
136     return oauth_server, oauth_request
137
138 def send_oauth_error(err=None):
139     """
140     Shortcut for sending an error.
141     """
142     response = HttpResponse(err.message.encode('utf-8'))
143     response.status_code = 401
144
145     realm = 'OAuth'
146     header = oauth.build_authenticate_header(realm=realm)
147
148     for k, v in header.iteritems():
149         response[k] = v
150
151     return response
152
153 def oauth_request_token(request):
154     oauth_server, oauth_request = initialize_server_request(request)
155     
156     if oauth_server is None:
157         return INVALID_PARAMS_RESPONSE
158     try:
159         token = oauth_server.fetch_request_token(oauth_request)
160
161         response = HttpResponse(token.to_string())
162     except oauth.OAuthError, err:
163         response = send_oauth_error(err)
164
165     return response
166
167 def oauth_auth_view(request, token, callback, params):
168     form = forms.OAuthAuthenticationForm(initial={
169         'oauth_token': token.key,
170         'oauth_callback': token.get_callback_url() or callback,
171       })
172
173     return render_to_response('piston/authorize_token.html',
174             { 'form': form }, RequestContext(request))
175
176 @login_required
177 def oauth_user_auth(request):
178     oauth_server, oauth_request = initialize_server_request(request)
179     
180     if oauth_request is None:
181         return INVALID_PARAMS_RESPONSE
182         
183     try:
184         token = oauth_server.fetch_request_token(oauth_request)
185     except oauth.OAuthError, err:
186         return send_oauth_error(err)
187         
188     try:
189         callback = oauth_server.get_callback(oauth_request)
190     except:
191         callback = None
192     
193     if request.method == "GET":
194         params = oauth_request.get_normalized_parameters()
195
196         oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', None)
197         if oauth_view is None:
198             return oauth_auth_view(request, token, callback, params)
199         else:
200             return get_callable(oauth_view)(request, token, callback, params)
201     elif request.method == "POST":
202         try:
203             form = forms.OAuthAuthenticationForm(request.POST)
204             if form.is_valid():
205                 token = oauth_server.authorize_token(token, request.user)
206                 args = '?'+token.to_string(only_key=True)
207             else:
208                 args = '?error=%s' % 'Access not granted by user.'
209                 print "FORM ERROR", form.errors
210             
211             if not callback:
212                 callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
213                 return get_callable(callback)(request, token)
214                 
215             response = HttpResponseRedirect(callback+args)
216                 
217         except oauth.OAuthError, err:
218             response = send_oauth_error(err)
219     else:
220         response = HttpResponse('Action not allowed.')
221             
222     return response
223
224 def oauth_access_token(request):
225     oauth_server, oauth_request = initialize_server_request(request)
226     
227     if oauth_request is None:
228         return INVALID_PARAMS_RESPONSE
229         
230     try:
231         token = oauth_server.fetch_access_token(oauth_request)
232         return HttpResponse(token.to_string())
233     except oauth.OAuthError, err:
234         return send_oauth_error(err)
235
236 INVALID_PARAMS_RESPONSE = send_oauth_error(oauth.OAuthError('Invalid request parameters.'))
237                 
238 class OAuthAuthentication(object):
239     """
240     OAuth authentication. Based on work by Leah Culver.
241     """
242     def __init__(self, realm='API'):
243         self.realm = realm
244         self.builder = oauth.build_authenticate_header
245     
246     def is_authenticated(self, request):
247         """
248         Checks whether a means of specifying authentication
249         is provided, and if so, if it is a valid token.
250         
251         Read the documentation on `HttpBasicAuthentication`
252         for more information about what goes on here.
253         """
254         if self.is_valid_request(request):
255             try:
256                 consumer, token, parameters = self.validate_token(request)
257             except oauth.OAuthError, err:
258                 print send_oauth_error(err)
259                 return False
260
261             if consumer and token:
262                 request.user = token.user
263                 request.consumer = consumer
264                 request.throttle_extra = token.consumer.id
265                 return True
266             
267         return False
268         
269     def challenge(self):
270         """
271         Returns a 401 response with a small bit on
272         what OAuth is, and where to learn more about it.
273         
274         When this was written, browsers did not understand
275         OAuth authentication on the browser side, and hence
276         the helpful template we render. Maybe some day in the
277         future, browsers will take care of this stuff for us
278         and understand the 401 with the realm we give it.
279         """
280         response = HttpResponse()
281         response.status_code = 401
282         realm = 'API'
283
284         for k, v in self.builder(realm=realm).iteritems():
285             response[k] = v
286
287         tmpl = loader.render_to_string('oauth/challenge.html',
288             { 'MEDIA_URL': settings.MEDIA_URL })
289
290         response.content = tmpl
291
292         return response
293         
294     @staticmethod
295     def is_valid_request(request):
296         """
297         Checks whether the required parameters are either in
298         the http-authorization header sent by some clients,
299         which is by the way the preferred method according to
300         OAuth spec, but otherwise fall back to `GET` and `POST`.
301         """
302         must_have = [ 'oauth_'+s for s in [
303             'consumer_key', 'token', 'signature',
304             'signature_method', 'timestamp', 'nonce' ] ]
305         
306         is_in = lambda l: all([ (p in l) for p in must_have ])
307
308         auth_params = request.META.get("HTTP_AUTHORIZATION", "")
309         req_params = request.REQUEST
310              
311         return is_in(auth_params) or is_in(req_params)
312         
313     @staticmethod
314     def validate_token(request, check_timestamp=True, check_nonce=True):
315         oauth_server, oauth_request = initialize_server_request(request)
316         return oauth_server.verify_request(oauth_request)
317