From 1434b3dfc0495e885921acb726df6cc06e00c5ed Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 8 Feb 2019 01:30:34 +0100 Subject: [PATCH 1/1] Move authorize endpoint to OAuthlib. --- src/api/drf_auth.py | 9 +-- src/api/piston_patch.py | 125 ++++++++--------------------------- src/api/request_validator.py | 27 +++++++- src/api/urls.py | 2 +- src/api/utils.py | 33 +++++++++ src/api/views.py | 20 ++---- 6 files changed, 98 insertions(+), 118 deletions(-) create mode 100644 src/api/utils.py diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py index ca6a49188..893f06610 100644 --- a/src/api/drf_auth.py +++ b/src/api/drf_auth.py @@ -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 diff --git a/src/api/piston_patch.py b/src/api/piston_patch.py index fb3987bde..3c7e50f4f 100644 --- a/src/api/piston_patch.py +++ b/src/api/piston_patch.py @@ -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 diff --git a/src/api/request_validator.py b/src/api/request_validator.py index b8554adef..6e3c0c22b 100644 --- a/src/api/request_validator.py +++ b/src/api/request_validator.py @@ -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 diff --git a/src/api/urls.py b/src/api/urls.py index 150dc4c0b..5936e1b64 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -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 index 000000000..6dc7e4505 --- /dev/null +++ b/src/api/utils.py @@ -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 diff --git a/src/api/views.py b/src/api/views.py index 6d462fa3a..3cf957ed9 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -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] -- 2.20.1