0002-use-drf-for-oauth2-APIs-16842.patch
fargo/oauth2/authentication.py | ||
---|---|---|
1 |
# fargo - document box |
|
2 |
# Copyright (C) 2016-2017 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.utils.translation import ugettext_lazy as _ |
|
18 | ||
19 |
from rest_framework.authentication import BasicAuthentication |
|
20 |
from rest_framework.exceptions import AuthenticationFailed |
|
21 | ||
22 |
from .models import OAuth2Client |
|
23 | ||
24 | ||
25 |
class OAuth2User(object): |
|
26 |
""" Fake user class to return in case OAuth2 Client authentication |
|
27 |
""" |
|
28 | ||
29 |
def __init__(self, oauth2_client): |
|
30 |
self.oauth2_client = oauth2_client |
|
31 |
self.authenticated = False |
|
32 | ||
33 |
def has_perm(self, *args, **kwargs): |
|
34 |
return True |
|
35 | ||
36 |
def has_perm_any(self, *args, **kwargs): |
|
37 |
return True |
|
38 | ||
39 |
def has_ou_perm(self, *args, **kwargs): |
|
40 |
return True |
|
41 | ||
42 |
def filter_by_perm(self, perms, queryset): |
|
43 |
return queryset |
|
44 | ||
45 |
def is_authenticated(self): |
|
46 |
return self.authenticated |
|
47 | ||
48 |
def is_staff(self): |
|
49 |
return False |
|
50 | ||
51 | ||
52 |
class FargoOAUTH2Authentication(BasicAuthentication): |
|
53 | ||
54 |
def authenticate_credentials(self, client_id, client_secret): |
|
55 |
try: |
|
56 |
client = OAuth2Client.objects.get( |
|
57 |
client_id=client_id, client_secret=client_secret) |
|
58 |
except OAuth2Client.DoesNotExist: |
|
59 |
raise AuthenticationFailed(_('Invalid client_id/client_secret.')) |
|
60 | ||
61 |
user = OAuth2User(client) |
|
62 |
user.authenticated = True |
|
63 |
return user, True |
fargo/oauth2/utils.py | ||
---|---|---|
1 | 1 |
import cgi |
2 |
import base64 |
|
3 | 2 |
from urllib import unquote |
4 | 3 | |
5 |
from .models import OAuth2Authorize, OAuth2Client
|
|
4 |
from .models import OAuth2Authorize |
|
6 | 5 | |
7 | 6 | |
8 | 7 |
def authenticate_bearer(request): |
... | ... | |
21 | 20 |
return False |
22 | 21 | |
23 | 22 | |
24 |
def authenticate_client(request, client=False): |
|
25 |
'''Authenticate client on the token endpoint''' |
|
26 | ||
27 |
if 'HTTP_AUTHORIZATION' in request.META: |
|
28 |
authorization = request.META['HTTP_AUTHORIZATION'].split() |
|
29 |
if authorization[0] != 'Basic' or len(authorization) != 2: |
|
30 |
return False |
|
31 |
try: |
|
32 |
decoded = base64.b64decode(authorization[1]) |
|
33 |
except TypeError: |
|
34 |
return False |
|
35 |
parts = decoded.split(':') |
|
36 |
if len(parts) != 2: |
|
37 |
return False |
|
38 |
client_id, client_secret = parts |
|
39 |
elif 'client_id' in request.POST: |
|
40 |
client_id = request.POST['client_id'] |
|
41 |
client_secret = request.POST.get('client_secret', '') |
|
42 |
else: |
|
43 |
return False |
|
44 |
if not client: |
|
45 |
try: |
|
46 |
client = OAuth2Client.objects.get(client_id=client_id) |
|
47 |
except OAuth2Client.DoesNotExist: |
|
48 |
return False |
|
49 |
if client.client_secret != client_secret: |
|
50 |
return False |
|
51 |
return client |
|
52 | ||
53 | ||
54 | 23 |
def get_content_disposition_value(request): |
55 | 24 |
if 'HTTP_CONTENT_DISPOSITION' not in request.META: |
56 | 25 |
return None, 'missing content-disposition header' |
fargo/oauth2/views.py | ||
---|---|---|
20 | 20 |
from django.core.files.base import ContentFile |
21 | 21 |
from django.core.urlresolvers import reverse |
22 | 22 |
from django.http import (HttpResponse, HttpResponseBadRequest, |
23 |
HttpResponseRedirect, JsonResponse)
|
|
23 |
HttpResponseRedirect) |
|
24 | 24 |
from django.views.decorators.csrf import csrf_exempt |
25 | 25 |
from django.views.generic import FormView, TemplateView |
26 | 26 | |
27 |
from rest_framework.response import Response |
|
28 |
from rest_framework.views import APIView |
|
29 | ||
30 |
from .authentication import FargoOAUTH2Authentication |
|
27 | 31 |
from .forms import OAuth2AuthorizeForm |
28 | 32 |
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile |
29 |
from .utils import authenticate_bearer, authenticate_client, get_content_disposition_value
|
|
33 |
from .utils import authenticate_bearer, get_content_disposition_value |
|
30 | 34 | |
31 | 35 |
from fargo.fargo.models import UserDocument, Document |
32 | 36 | |
... | ... | |
35 | 39 |
pass |
36 | 40 | |
37 | 41 | |
42 |
class OAUTH2APIViewMixin(APIView): |
|
43 |
http_method_names = ['post'] |
|
44 |
authentication_classes = (FargoOAUTH2Authentication,) |
|
45 | ||
46 |
@csrf_exempt |
|
47 |
def dispatch(self, request, *args, **kwargs): |
|
48 |
return super(OAUTH2APIViewMixin, self).dispatch(request, *args, **kwargs) |
|
49 | ||
50 | ||
38 | 51 |
class OAuth2AuthorizeView(FormView): |
39 | 52 |
template_name = 'oauth2/authorize.html' |
40 | 53 |
form_class = OAuth2AuthorizeForm |
... | ... | |
98 | 111 |
return super(OAuth2AuthorizeView, self).form_valid(form) |
99 | 112 | |
100 | 113 | |
101 |
@csrf_exempt |
|
102 |
def get_document_token(request): |
|
103 |
grant_type = request.POST.get('grant_type') |
|
104 |
redirect_uri = request.POST.get('redirect_uri') |
|
105 |
code = request.POST.get('code') |
|
106 |
client = authenticate_client(request) |
|
114 |
class GetDocumentTokenView(OAUTH2APIViewMixin): |
|
115 | ||
116 |
def post(self, request, *args, **kwargs): |
|
117 |
client = request.user.oauth2_client |
|
118 |
data = request.data |
|
119 |
if not client.check_redirect_uri(data['redirect_uri']): |
|
120 |
return Response({'error': 'invalid_request'}, status=400) |
|
121 |
if data['grant_type'] != 'authorization_code': |
|
122 |
return Response({'error': 'unsopported_grant_type'}, status=400) |
|
123 | ||
124 |
try: |
|
125 |
token = OAuth2Authorize.objects.get(code=data['code']).access_token |
|
126 |
except OAuth2Authorize.DoesNotExist: |
|
127 |
return Response({'error': 'invalid_request'}, status=400) |
|
107 | 128 | |
108 |
try: |
|
109 |
if not client: |
|
110 |
raise OAuth2Exception('invalid client') |
|
111 |
if redirect_uri not in client.get_redirect_uris(): |
|
112 |
raise OAuth2Exception('invalid_request') |
|
113 |
if grant_type != 'authorization_code': |
|
114 |
raise OAuth2Exception('unsupported_grant_type') |
|
115 |
except OAuth2Exception as e: |
|
116 |
return JsonResponse({'error': e.message}, status=400) |
|
129 |
return Response({'access_token': token, 'expires': '3600'}) |
|
117 | 130 | |
118 |
try: |
|
119 |
doc_token = OAuth2Authorize.objects.get(code=code).access_token |
|
120 |
except OAuth2Authorize.DoesNotExist: |
|
121 |
return JsonResponse({'error': 'invalid_request'}, status=400) |
|
122 | 131 | |
123 |
return JsonResponse({'access_token': doc_token, 'expires': '3600'})
|
|
132 |
get_document_token = GetDocumentTokenView.as_view()
|
|
124 | 133 | |
125 | 134 | |
126 | 135 |
def get_document(request): |
... | ... | |
139 | 148 |
return response |
140 | 149 | |
141 | 150 | |
142 |
@csrf_exempt |
|
143 |
def put_document(request): |
|
144 |
client = authenticate_client(request) |
|
145 |
if not client: |
|
146 |
return HttpResponseBadRequest('basic HTTP authentication failed') |
|
151 |
class PutDocumentAPIView(OAUTH2APIViewMixin): |
|
152 | ||
153 |
def post(self, request, *args, **kwargs): |
|
154 |
filename, error = get_content_disposition_value(request) |
|
155 |
if error: |
|
156 |
return HttpResponseBadRequest(error) |
|
147 | 157 | |
148 |
filename, error = get_content_disposition_value(request) |
|
149 |
if error: |
|
150 |
return HttpResponseBadRequest(error) |
|
158 |
f = ContentFile(request.body, name=filename) |
|
159 |
document = Document.objects.get_by_file(f) |
|
160 |
oauth2_document = OAuth2TempFile.objects.create(document=document, |
|
161 |
filename=filename) |
|
162 |
uri = reverse('oauth2-put-document-authorize', |
|
163 |
args=[oauth2_document.pk]) + '/' |
|
151 | 164 | |
152 |
f = ContentFile(request.body, name=filename) |
|
153 |
document = Document.objects.get_by_file(f) |
|
154 |
oauth2_document = OAuth2TempFile.objects.create(document=document, |
|
155 |
filename=filename) |
|
156 |
uri = reverse('oauth2-put-document-authorize', |
|
157 |
args=[oauth2_document.pk]) + '/' |
|
165 |
response = Response() |
|
166 |
response['Location'] = uri |
|
167 |
return response |
|
158 | 168 | |
159 |
response = HttpResponse() |
|
160 |
response['Location'] = uri |
|
161 |
return response |
|
169 | ||
170 |
put_document = PutDocumentAPIView.as_view() |
|
162 | 171 | |
163 | 172 | |
164 | 173 |
class OAuth2AuthorizePutView(TemplateView): |
tests/test_oauth2.py | ||
---|---|---|
86 | 86 |
params['code'] = auth.code |
87 | 87 | |
88 | 88 |
url = reverse('oauth2-get-token') |
89 |
app.authorization = ('Basic', (oauth2_client.client_id, oauth2_client.client_secret)) |
|
89 | 90 |
resp = app.post(url, params=params, status=200) |
90 | 91 |
assert 'access_token' in resp.json |
91 | 92 |
assert 'expires' in resp.json |
... | ... | |
109 | 110 |
data = f.read() |
110 | 111 | |
111 | 112 |
url = reverse('oauth2-put-document') |
112 |
resp = app.post(url, params=data, status=400) |
|
113 |
assert 'basic HTTP authentication failed' in resp.content |
|
113 |
resp = app.post(url, params=data, status=401) |
|
114 | 114 | |
115 | 115 |
app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret))) |
116 | 116 |
resp = app.post(url, params=data, status=400) |
117 |
- |