0001-use-drf-for-oauth2-APIs-16842.patch
fargo/oauth2/rest_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 |
import base64 |
|
18 | ||
19 |
from django.utils.translation import ugettext_lazy as _ |
|
20 | ||
21 |
from rest_framework.authentication import BaseAuthentication |
|
22 |
from rest_framework.exceptions import AuthenticationFailed |
|
23 | ||
24 |
from .models import OAuth2Client |
|
25 | ||
26 | ||
27 |
class OAuth2User(object): |
|
28 |
""" Fake user class to return in case OAuth2 Client authentication |
|
29 |
""" |
|
30 | ||
31 |
def __init__(self, oauth2_client): |
|
32 |
self.oauth2_client = oauth2_client |
|
33 |
self.authenticated = False |
|
34 | ||
35 |
def has_perm(self, *args, **kwargs): |
|
36 |
return True |
|
37 | ||
38 |
def has_perm_any(self, *args, **kwargs): |
|
39 |
return True |
|
40 | ||
41 |
def has_ou_perm(self, *args, **kwargs): |
|
42 |
return True |
|
43 | ||
44 |
def filter_by_perm(self, perms, queryset): |
|
45 |
return queryset |
|
46 | ||
47 |
def is_authenticated(self): |
|
48 |
return self.authenticated |
|
49 | ||
50 |
def is_staff(self): |
|
51 |
return False |
|
52 | ||
53 | ||
54 |
class FargoOAUTH2Authentication(BaseAuthentication): |
|
55 | ||
56 |
def authenticate_header(self, request): |
|
57 |
return 'basic HTTP authentication failed' |
|
58 | ||
59 |
def authenticate(self, request): |
|
60 |
'''Authenticate client on the token endpoint''' |
|
61 | ||
62 |
if 'HTTP_AUTHORIZATION' in request.META: |
|
63 |
authorization = request.META['HTTP_AUTHORIZATION'].split() |
|
64 |
if authorization[0] != 'Basic' or len(authorization) != 2: |
|
65 |
return False |
|
66 |
try: |
|
67 |
decoded = base64.b64decode(authorization[1]) |
|
68 |
except TypeError: |
|
69 |
return False |
|
70 |
parts = decoded.split(':') |
|
71 |
if len(parts) != 2: |
|
72 |
return False |
|
73 |
client_id, client_secret = parts |
|
74 |
elif 'client_id' in request.POST: |
|
75 |
client_id = request.POST['client_id'] |
|
76 |
client_secret = request.POST.get('client_secret', '') |
|
77 |
else: |
|
78 |
raise AuthenticationFailed(self.authenticate_header(request)) |
|
79 | ||
80 |
return self.authenticate_credentials(client_id, client_secret) |
|
81 | ||
82 |
def authenticate_credentials(self, client_id, client_secret): |
|
83 |
try: |
|
84 |
client = OAuth2Client.objects.get( |
|
85 |
client_id=client_id, client_secret=client_secret) |
|
86 |
except OAuth2Client.DoesNotExist: |
|
87 |
raise AuthenticationFailed(_('Invalid client_id/client_secret.')) |
|
88 | ||
89 |
user = OAuth2User(client) |
|
90 |
user.authenticated = True |
|
91 |
return user, True |
fargo/oauth2/utils.py | ||
---|---|---|
1 |
import cgi |
|
2 |
from urllib import unquote |
|
3 | ||
4 |
from .models import OAuth2Authorize |
|
5 | ||
6 | ||
7 |
def authenticate_bearer(request): |
|
8 |
authorization = request.META.get('HTTP_AUTHORIZATION') |
|
9 |
if not authorization: |
|
10 |
return False |
|
11 |
splitted = authorization.split() |
|
12 |
if len(splitted) < 2: |
|
13 |
return False |
|
14 |
if splitted[0] != 'Bearer': |
|
15 |
return False |
|
16 |
token = splitted[1] |
|
17 |
try: |
|
18 |
return OAuth2Authorize.objects.get(access_token=token) |
|
19 |
except OAuth2Authorize.DoesNotExist: |
|
20 |
return False |
|
21 | ||
22 | ||
23 |
def get_content_disposition_value(request): |
|
24 |
if 'HTTP_CONTENT_DISPOSITION' not in request.META: |
|
25 |
return None, 'missing content-disposition header' |
|
26 |
content_header = request.META['HTTP_CONTENT_DISPOSITION'] |
|
27 |
disposition_type, filename = cgi.parse_header(content_header) |
|
28 |
if disposition_type != 'attachement': |
|
29 |
return None, 'wrong disposition type: attachement excpected' |
|
30 |
if 'filename*' in filename: |
|
31 |
encode, country, name = filename['filename*'].split("'") |
|
32 | ||
33 |
# check accepted charset from rfc 5987 |
|
34 |
if encode == 'UTF-8': |
|
35 |
return unquote(name.decode('utf8')), None |
|
36 |
elif encode == 'ISO-8859-1': |
|
37 |
return unquote(name.decode('iso-8859-1')), None |
|
38 |
else: |
|
39 |
return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed' |
|
40 |
elif 'filename' in filename: |
|
41 |
return filename['filename'], None |
|
42 |
else: |
|
43 |
# no filename in header |
|
44 |
return None, 'missing filename(*) parameter in header' |
fargo/oauth2/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import cgi |
|
18 |
import base64 |
|
19 | 17 |
import urllib |
20 |
from urllib import quote, unquote
|
|
18 |
from urllib import quote |
|
21 | 19 | |
22 | 20 |
from django.core.files.base import ContentFile |
23 | 21 |
from django.core.urlresolvers import reverse |
24 | 22 |
from django.http import (HttpResponse, HttpResponseBadRequest, |
25 |
HttpResponseRedirect, JsonResponse)
|
|
23 |
HttpResponseRedirect) |
|
26 | 24 |
from django.views.decorators.csrf import csrf_exempt |
27 | 25 |
from django.views.generic import FormView, TemplateView |
28 | 26 | |
27 |
from rest_framework import serializers |
|
28 |
from rest_framework.response import Response |
|
29 |
from rest_framework.views import APIView |
|
30 | ||
29 | 31 |
from .forms import OAuth2AuthorizeForm |
30 | 32 |
from .models import OAuth2Authorize, OAuth2Client, OAuth2TempFile |
33 |
from .utils import authenticate_bearer, get_content_disposition_value |
|
31 | 34 | |
32 | 35 |
from fargo.fargo.models import UserDocument, Document |
36 |
from fargo.oauth2.rest_authentication import FargoOAUTH2Authentication |
|
33 | 37 | |
34 | 38 | |
35 | 39 |
class OAuth2Exception(Exception): |
36 | 40 |
pass |
37 | 41 | |
38 | 42 | |
43 |
class OAuth2ClientSerializer(serializers.ModelSerializer): |
|
44 | ||
45 |
class Meta: |
|
46 |
model = OAuth2Client |
|
47 |
fields = ('client_id', 'client_secret') |
|
48 | ||
49 | ||
50 |
class OAUTH2ClientApiViewMixin(APIView): |
|
51 |
http_method_names = ['post'] |
|
52 |
authentication_classes = (FargoOAUTH2Authentication,) |
|
53 | ||
54 |
@csrf_exempt |
|
55 |
def dispatch(self, request, *args, **kwargs): |
|
56 |
return super(OAUTH2ClientApiViewMixin, self).dispatch(request, *args, **kwargs) |
|
57 | ||
58 | ||
39 | 59 |
class OAuth2AuthorizeView(FormView): |
40 | 60 |
template_name = 'oauth2/authorize.html' |
41 | 61 |
form_class = OAuth2AuthorizeForm |
... | ... | |
99 | 119 |
return super(OAuth2AuthorizeView, self).form_valid(form) |
100 | 120 | |
101 | 121 | |
102 |
@csrf_exempt |
|
103 |
def get_document_token(request): |
|
104 |
grant_type = request.POST.get('grant_type') |
|
105 |
redirect_uri = request.POST.get('redirect_uri') |
|
106 |
code = request.POST.get('code') |
|
107 |
client = authenticate_client(request) |
|
122 |
class GetDocumentTokenView(OAUTH2ClientApiViewMixin): |
|
123 | ||
124 |
def post(self, request, *args, **kwargs): |
|
125 |
client = request.user.oauth2_client |
|
126 |
data = request.data |
|
127 |
if not client.check_redirect_uri(data['redirect_uri']): |
|
128 |
return Response({'error': 'invalid_request'}, status=400) |
|
129 |
if data['grant_type'] != 'authorization_code': |
|
130 |
return Response({'error': 'unsopported_grant_type'}, status=400) |
|
131 | ||
132 |
try: |
|
133 |
token = OAuth2Authorize.objects.get(code=data['code']).access_token |
|
134 |
except OAuth2Authorize.DoesNotExist: |
|
135 |
return Response({'error': 'invalid_request'}, status=400) |
|
108 | 136 | |
109 |
try: |
|
110 |
if not client: |
|
111 |
raise OAuth2Exception('invalid client') |
|
112 |
if redirect_uri not in client.get_redirect_uris(): |
|
113 |
raise OAuth2Exception('invalid_request') |
|
114 |
if grant_type != 'authorization_code': |
|
115 |
raise OAuth2Exception('unsupported_grant_type') |
|
116 |
except OAuth2Exception as e: |
|
117 |
return JsonResponse({'error': e.message}, status=400) |
|
137 |
return Response({'access_token': token, 'expires': '3600'}) |
|
118 | 138 | |
119 |
try: |
|
120 |
doc_token = OAuth2Authorize.objects.get(code=code).access_token |
|
121 |
except OAuth2Authorize.DoesNotExist: |
|
122 |
return JsonResponse({'error': 'invalid_request'}, status=400) |
|
123 | 139 | |
124 |
return JsonResponse({'access_token': doc_token, 'expires': '3600'})
|
|
140 |
get_document_token = GetDocumentTokenView.as_view()
|
|
125 | 141 | |
126 | 142 | |
127 | 143 |
def get_document(request): |
... | ... | |
140 | 156 |
return response |
141 | 157 | |
142 | 158 | |
143 |
def authenticate_bearer(request): |
|
144 |
authorization = request.META.get('HTTP_AUTHORIZATION') |
|
145 |
if not authorization: |
|
146 |
return False |
|
147 |
splitted = authorization.split() |
|
148 |
if len(splitted) < 2: |
|
149 |
return False |
|
150 |
if splitted[0] != 'Bearer': |
|
151 |
return False |
|
152 |
token = splitted[1] |
|
153 |
try: |
|
154 |
return OAuth2Authorize.objects.get(access_token=token) |
|
155 |
except OAuth2Authorize.DoesNotExist: |
|
156 |
return False |
|
157 | ||
158 | ||
159 |
def authenticate_client(request, client=False): |
|
160 |
'''Authenticate client on the token endpoint''' |
|
161 | ||
162 |
if 'HTTP_AUTHORIZATION' in request.META: |
|
163 |
authorization = request.META['HTTP_AUTHORIZATION'].split() |
|
164 |
if authorization[0] != 'Basic' or len(authorization) != 2: |
|
165 |
return False |
|
166 |
try: |
|
167 |
decoded = base64.b64decode(authorization[1]) |
|
168 |
except TypeError: |
|
169 |
return False |
|
170 |
parts = decoded.split(':') |
|
171 |
if len(parts) != 2: |
|
172 |
return False |
|
173 |
client_id, client_secret = parts |
|
174 |
elif 'client_id' in request.POST: |
|
175 |
client_id = request.POST['client_id'] |
|
176 |
client_secret = request.POST.get('client_secret', '') |
|
177 |
else: |
|
178 |
return False |
|
179 |
if not client: |
|
180 |
try: |
|
181 |
client = OAuth2Client.objects.get(client_id=client_id) |
|
182 |
except OAuth2Client.DoesNotExist: |
|
183 |
return False |
|
184 |
if client.client_secret != client_secret: |
|
185 |
return False |
|
186 |
return client |
|
187 | ||
188 | ||
189 |
def get_content_disposition_value(request): |
|
190 |
if 'HTTP_CONTENT_DISPOSITION' not in request.META: |
|
191 |
return None, 'missing content-disposition header' |
|
192 |
content_header = request.META['HTTP_CONTENT_DISPOSITION'] |
|
193 |
disposition_type, filename = cgi.parse_header(content_header) |
|
194 |
if disposition_type != 'attachement': |
|
195 |
return None, 'wrong disposition type: attachement excpected' |
|
196 |
if 'filename*' in filename: |
|
197 |
encode, country, name = filename['filename*'].split("'") |
|
198 | ||
199 |
# check accepted charset from rfc 5987 |
|
200 |
if encode == 'UTF-8': |
|
201 |
return unquote(name.decode('utf8')), None |
|
202 |
elif encode == 'ISO-8859-1': |
|
203 |
return unquote(name.decode('iso-8859-1')), None |
|
204 |
else: |
|
205 |
return None, 'unknown encoding: UTF-8 or ISO-8859-1 allowed' |
|
206 |
elif 'filename' in filename: |
|
207 |
return filename['filename'], None |
|
208 |
else: |
|
209 |
# no filename in header |
|
210 |
return None, 'missing filename(*) parameter in header' |
|
211 | ||
212 | ||
213 |
@csrf_exempt |
|
214 |
def put_document(request): |
|
215 |
client = authenticate_client(request) |
|
216 |
if not client: |
|
217 |
return HttpResponseBadRequest('basic HTTP authentication failed') |
|
218 | ||
219 |
filename, error = get_content_disposition_value(request) |
|
220 |
if error: |
|
221 |
return HttpResponseBadRequest(error) |
|
222 | ||
223 |
f = ContentFile(request.body, name=filename) |
|
224 |
document = Document.objects.get_by_file(f) |
|
225 |
oauth2_document = OAuth2TempFile.objects.create(document=document, |
|
226 |
filename=filename) |
|
227 |
uri = reverse('oauth2-put-document-authorize', |
|
228 |
args=[oauth2_document.pk]) + '/' |
|
229 | ||
230 |
response = HttpResponse() |
|
231 |
response['Location'] = uri |
|
232 |
return response |
|
159 |
class PutDocumentAPIView(OAUTH2ClientApiViewMixin): |
|
160 | ||
161 |
def post(self, request, *args, **kwargs): |
|
162 |
filename, error = get_content_disposition_value(request) |
|
163 |
if error: |
|
164 |
return HttpResponseBadRequest(error) |
|
165 | ||
166 |
f = ContentFile(request.body, name=filename) |
|
167 |
document = Document.objects.get_by_file(f) |
|
168 |
oauth2_document = OAuth2TempFile.objects.create(document=document, |
|
169 |
filename=filename) |
|
170 |
uri = reverse('oauth2-put-document-authorize', |
|
171 |
args=[oauth2_document.pk]) + '/' |
|
172 | ||
173 |
response = Response() |
|
174 |
response['Location'] = uri |
|
175 |
return response |
|
176 | ||
177 | ||
178 |
put_document = PutDocumentAPIView.as_view() |
|
233 | 179 | |
234 | 180 | |
235 | 181 |
class OAuth2AuthorizePutView(TemplateView): |
tests/test_oauth2.py | ||
---|---|---|
109 | 109 |
data = f.read() |
110 | 110 | |
111 | 111 |
url = reverse('oauth2-put-document') |
112 |
resp = app.post(url, params=data, status=400)
|
|
112 |
resp = app.post(url, params=data, status=401)
|
|
113 | 113 |
assert 'basic HTTP authentication failed' in resp.content |
114 | 114 | |
115 | 115 |
app.authorization = ('Basic', (str(oauth2_client.client_id), str(oauth2_client.client_secret))) |
116 |
- |