Move authorize endpoint to OAuthlib.
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 8 Feb 2019 00:30:34 +0000 (01:30 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 8 Feb 2019 00:30:34 +0000 (01:30 +0100)
src/api/drf_auth.py
src/api/piston_patch.py
src/api/request_validator.py
src/api/urls.py
src/api/utils.py [new file with mode: 0644]
src/api/views.py

index ca6a491..893f066 100644 (file)
@@ -5,6 +5,7 @@
 from oauthlib.oauth1 import ResourceEndpoint
 from rest_framework.authentication import BaseAuthentication
 from .request_validator import PistonRequestValidator
+from .utils import oauthlib_request
 
 
 class PistonOAuthAuthentication(BaseAuthentication):
@@ -17,13 +18,7 @@ class PistonOAuthAuthentication(BaseAuthentication):
 
     def authenticate(self, request):
         v, r = self.provider.validate_protected_resource_request(
-            request.build_absolute_uri(),
-            http_method=request.method,
-            body=request.body,
-            headers={
-                "Authorization": request.META['HTTP_AUTHORIZATION'],
-                "Content-Type": request.content_type,
-            } if 'HTTP_AUTHORIZATION' in request.META else None
+            **oauthlib_request(request)
         )
         if v:
             return r.token.user, r.token
index fb3987b..3c7e50f 100644 (file)
@@ -1,18 +1,14 @@
 # -*- coding: utf-8 -*-
-
-# modified from django-piston
-import base64
-import hmac
-
-from django import forms
-from django.conf import settings
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from oauthlib.oauth1 import AuthorizationEndpoint
 from django.contrib.auth.decorators import login_required
-from django.core.urlresolvers import get_callable
-from django.http import HttpResponseRedirect, HttpResponse
-from django.shortcuts import render_to_response
-from django.template.context import RequestContext
-from piston import oauth
-from piston.authentication import initialize_server_request, INVALID_PARAMS_RESPONSE, send_oauth_error
+from django import forms
+from django.http import HttpResponseRedirect
+from django.shortcuts import render
+from .request_validator import PistonRequestValidator
+from .utils import oauthlib_request, oauthlib_response
 
 
 class HttpResponseAppRedirect(HttpResponseRedirect):
@@ -23,95 +19,32 @@ class OAuthAuthenticationForm(forms.Form):
     oauth_token = forms.CharField(widget=forms.HiddenInput)
     oauth_callback = forms.CharField(widget=forms.HiddenInput)  # changed from URLField - too strict
     # removed authorize_access - redundant
-    csrf_signature = forms.CharField(widget=forms.HiddenInput)
-
-    def __init__(self, *args, **kwargs):
-        forms.Form.__init__(self, *args, **kwargs)
-
-        self.fields['csrf_signature'].initial = self.initial_csrf_signature
-
-    def clean_csrf_signature(self):
-        sig = self.cleaned_data['csrf_signature']
-        token = self.cleaned_data['oauth_token']
-
-        sig1 = OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token)
-
-        if sig != sig1:
-            raise forms.ValidationError("CSRF signature is not valid")
-
-        return sig
-
-    def initial_csrf_signature(self):
-        token = self.initial['oauth_token']
-        return OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, token)
-
-    @staticmethod
-    def get_csrf_signature(key, token):
-        # Check signature...
-        import hashlib  # 2.5
-        hashed = hmac.new(key, token, hashlib.sha1)
-
-        # calculate the digest base 64
-        return base64.b64encode(hashed.digest())
-
-
-# The only thing changed in the views below is the form used
-# and also the Http Redirect class
-
-
-def oauth_auth_view(request, token, callback, params):
-    form = OAuthAuthenticationForm(initial={
-        'oauth_token': token.key,
-        'oauth_callback': callback,
-    })
-
-    return render_to_response('piston/authorize_token.html',
-                              {'form': form}, RequestContext(request))
 
 
 @login_required
 def oauth_user_auth(request):
-    oauth_server, oauth_request = initialize_server_request(request)
-
-    if oauth_request is None:
-        return INVALID_PARAMS_RESPONSE
-
-    try:
-        token = oauth_server.fetch_request_token(oauth_request)
-    except oauth.OAuthError, err:
-        return send_oauth_error(err)
-
-    try:
-        callback = oauth_server.get_callback(oauth_request)
-    except:
-        callback = None
+    endpoint = AuthorizationEndpoint(PistonRequestValidator())
 
     if request.method == "GET":
-        params = oauth_request.get_normalized_parameters()
-
-        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', None)
-        if oauth_view is None:
-            return oauth_auth_view(request, token, callback, params)
-        else:
-            return get_callable(oauth_view)(request, token, callback, params)
-    elif request.method == "POST":
-        try:
-            form = OAuthAuthenticationForm(request.POST)
-            if form.is_valid():
-                token = oauth_server.authorize_token(token, request.user)
-                args = '?' + token.to_string(only_key=True)
-            else:
-                args = '?error=%s' % 'Access not granted by user.'
-
-            if not callback:
-                callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
-                return get_callable(callback)(request, token)
+        # Why not just get oauth_token here?
+        # This is fairly straightforward, in't?
+        realms, credentials = endpoint.get_realms_and_credentials(
+            **oauthlib_request(request))
+        callback = request.GET.get('oauth_callback')
 
-            response = HttpResponseAppRedirect(callback + args)
+        form = OAuthAuthenticationForm(initial={
+            'oauth_token': credentials['resource_owner_key'],
+            'oauth_callback': callback,
+        })
 
-        except oauth.OAuthError, err:
-            response = send_oauth_error(err)
-    else:
-        response = HttpResponse('Action not allowed.')
+        return render(request, 'piston/authorize_token.html', {'form': form})
 
-    return response
+    elif request.method == "POST":
+        response = oauthlib_response(
+            endpoint.create_authorization_response(
+                credentials={"user": request.user},
+                **oauthlib_request(request)
+            )
+        )
+
+        return response
index b8554ad..6e3c0c2 100644 (file)
@@ -2,11 +2,14 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+import time
 from oauthlib.oauth1 import RequestValidator
 from piston.models import Consumer, Nonce, Token
 
 
 class PistonRequestValidator(RequestValidator):
+    timestamp_threshold = 300
+
     dummy_access_token = '!'
     realms = ['API']
 
@@ -45,7 +48,8 @@ class PistonRequestValidator(RequestValidator):
 
     def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
                                      request, request_token=None, access_token=None):
-        # TODO: validate the timestamp
+        if abs(time.time() - int(timestamp)) > self.timestamp_threshold:
+            return False
         token = request_token or access_token
         # Yes, this is what Piston did.
         if token is None:
@@ -83,3 +87,24 @@ class PistonRequestValidator(RequestValidator):
             secret=token['oauth_token_secret'],
             consumer=request.oauth_consumer,
         )
+
+    def verify_request_token(self, token, request):
+        return Token.objects.filter(
+            token_type=Token.REQUEST, key=token, is_approved=False
+        ).exists()
+
+    def get_realms(self, *args, **kwargs):
+        return []
+
+    def save_verifier(self, token, verifier, request):
+        Token.objects.filter(
+            token_type=Token.REQUEST,
+            key=token,
+            is_approved=False
+        ).update(
+            is_approved=True,
+            user=verifier['user']
+        )
+
+    def get_redirect_uri(self, token, request):
+        return request.redirect_uri
index 150dc4c..5936e1b 100644 (file)
@@ -5,7 +5,7 @@
 from django.conf.urls import url, include
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import TemplateView
-from piston.authentication import oauth_access_token, oauth_request_token
+from piston.authentication import oauth_access_token
 import catalogue.views
 from api import handlers
 from api.piston_patch import oauth_user_auth
diff --git a/src/api/utils.py b/src/api/utils.py
new file mode 100644 (file)
index 0000000..6dc7e45
--- /dev/null
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.http import HttpResponse
+from django.utils.encoding import iri_to_uri
+
+
+def oauthlib_request(request):
+    """Creates parameters for OAuthlib's Request from a Django Request."""
+    headers = {}
+    # We don't have request.content_type yet in 2015,
+    # while test client has no META['CONTENT_TYPE'].
+    ct = request.META.get('CONTENT_TYPE', getattr(request, 'content_type', None))
+    if ct:
+        headers["Content-Type"] = ct
+    if 'HTTP_AUTHORIZATION' in request.META:
+        headers["Authorization"] = request.META['HTTP_AUTHORIZATION']
+    return {
+        "uri": request.build_absolute_uri(),
+        "http_method": request.method,
+        "body": request.body,
+        "headers": headers,
+    }
+
+def oauthlib_response((headers, body, status)):
+    """Creates a django.http.HttpResponse from (headers, body, status) tuple from OAuthlib."""
+    response = HttpResponse(body, status=status)
+    for k, v in headers.items():
+        if k == 'Location':
+            v = iri_to_uri(v)
+        response[k] = v
+    return response
index 6d462fa..3cf957e 100644 (file)
@@ -2,7 +2,7 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from django.http import Http404, HttpResponse
+from django.http import Http404
 from oauthlib.common import urlencode
 from oauthlib.oauth1 import RequestTokenEndpoint
 from piston.models import KEY_SIZE, SECRET_SIZE
@@ -15,6 +15,7 @@ from catalogue.models import Book
 from .models import BookUserData
 from . import serializers
 from .request_validator import PistonRequestValidator
+from .utils import oauthlib_request, oauthlib_response
 
 
 class OAuth1RequestTokenEndpoint(RequestTokenEndpoint):
@@ -36,21 +37,14 @@ class OAuth1RequestTokenEndpoint(RequestTokenEndpoint):
 class OAuth1RequestTokenView(APIView):
     def __init__(self):
         self.endpoint = OAuth1RequestTokenEndpoint(PistonRequestValidator())
+
     def dispatch(self, request):
-        headers, body, status = self.endpoint.create_request_token_response(
-            request.build_absolute_uri(),
-            request.method,
-            request.body,
-            {
-                "Authorization": request.META['HTTP_AUTHORIZATION']
-            } if 'HTTP_AUTHORIZATION' in request.META else None
+        return oauthlib_response(
+            self.endpoint.create_request_token_response(
+                **oauthlib_request(request)
+            )
         )
 
-        response = HttpResponse(body, status=status)
-        for k, v in headers.items():
-            response[k] = v
-        return response
-
 
 class UserView(RetrieveAPIView):
     permission_classes = [IsAuthenticated]