0
# Django REST Framework Integration
1
2
Django OAuth Toolkit provides first-class integration with Django REST Framework, offering OAuth2 authentication and comprehensive permission classes for API protection. This integration enables token-based API authentication with fine-grained scope validation.
3
4
## Capabilities
5
6
### OAuth2 Authentication Backend
7
8
DRF authentication backend that validates OAuth2 access tokens and provides user context.
9
10
```python { .api }
11
class OAuth2Authentication(BaseAuthentication):
12
www_authenticate_realm: str = "api"
13
14
def authenticate(self, request) -> Optional[Tuple[User, AccessToken]]:
15
"""
16
Authenticate request using OAuth2 access token.
17
18
Args:
19
request: DRF Request object
20
21
Returns:
22
Tuple of (user, token) if authentication succeeds, None otherwise
23
"""
24
25
def authenticate_header(self, request) -> str:
26
"""Return WWW-Authenticate header value for OAuth2"""
27
```
28
29
### Token Scope Permission Classes
30
31
Permission classes that validate OAuth2 token scopes for API endpoint protection.
32
33
```python { .api }
34
class TokenHasScope(BasePermission):
35
"""
36
Permission class that requires token to have specific scopes.
37
38
Validates that the OAuth2 access token has required scopes.
39
Must be used with OAuth2Authentication.
40
41
Attributes:
42
required_scopes: List of required scope names
43
message: Custom error message for permission denied
44
45
Usage:
46
@permission_classes([TokenHasScope])
47
@required_scopes(['read'])
48
def my_view(request):
49
# Requires 'read' scope
50
pass
51
52
class MyAPIView(APIView):
53
permission_classes = [TokenHasScope]
54
required_scopes = ['write']
55
"""
56
57
required_scopes = []
58
59
def has_permission(self, request, view) -> bool:
60
"""
61
Check if token has required scopes.
62
63
Args:
64
request: DRF Request object with OAuth2 authentication
65
view: DRF View instance
66
67
Returns:
68
True if token has all required scopes
69
"""
70
71
def get_scopes(self, request, view) -> list:
72
"""
73
Get required scopes for this request/view.
74
75
Args:
76
request: DRF Request object
77
view: DRF View instance
78
79
Returns:
80
List of required scope names
81
"""
82
83
class TokenHasReadWriteScope(TokenHasScope):
84
"""
85
Permission class with automatic read/write scope assignment.
86
87
Safe HTTP methods (GET, HEAD, OPTIONS) require 'read' scope.
88
Unsafe HTTP methods require 'write' scope.
89
Additional scopes can be specified via required_scopes.
90
91
Usage:
92
@permission_classes([TokenHasReadWriteScope])
93
def api_view(request):
94
# GET requests need 'read' scope
95
# POST/PUT/DELETE need 'write' scope
96
pass
97
"""
98
99
def get_scopes(self, request, view) -> list:
100
"""Get read/write scopes based on HTTP method"""
101
102
class TokenHasResourceScope(TokenHasScope):
103
"""
104
Permission class for resource-specific scope validation.
105
106
Validates scopes based on the specific resource being accessed.
107
Useful for APIs with multiple resource types.
108
109
Attributes:
110
required_alternate_scopes: Dict mapping resources to required scopes
111
112
Usage:
113
class MyView(APIView):
114
permission_classes = [TokenHasResourceScope]
115
required_alternate_scopes = {
116
'users': ['user:read'],
117
'posts': ['post:read'],
118
}
119
"""
120
121
required_alternate_scopes = {}
122
123
def get_required_alternate_scopes(self, request, view) -> dict:
124
"""Get resource-specific scope requirements"""
125
126
class IsAuthenticatedOrTokenHasScope(BasePermission):
127
"""
128
Permission allowing access to authenticated users or valid token holders.
129
130
Grants access if user is authenticated via session OR has valid OAuth2 token.
131
If using OAuth2 token, validates required scopes.
132
133
Attributes:
134
required_scopes: Scopes required for OAuth2 token access
135
136
Usage:
137
# Allow both session auth and OAuth2 token auth
138
@permission_classes([IsAuthenticatedOrTokenHasScope])
139
@required_scopes(['api'])
140
def hybrid_view(request):
141
pass
142
"""
143
144
required_scopes = []
145
146
def has_permission(self, request, view) -> bool:
147
"""Check session authentication or token scopes"""
148
149
class TokenMatchesOASRequirements(BasePermission):
150
"""
151
Permission class for OpenAPI Specification (OAS) scope matching.
152
153
Validates OAuth2 scopes against OpenAPI security requirements.
154
Useful for APIs documented with OpenAPI/Swagger specifications.
155
156
Attributes:
157
required_alternate_scopes: OpenAPI-style scope alternatives
158
159
Usage:
160
# Matches OpenAPI security requirement alternatives
161
class APIView(APIView):
162
permission_classes = [TokenMatchesOASRequirements]
163
required_alternate_scopes = {
164
'oauth2': ['read', 'write']
165
}
166
"""
167
168
required_alternate_scopes = {}
169
170
def has_permission(self, request, view) -> bool:
171
"""Validate against OAS-style scope requirements"""
172
```
173
174
## Usage Examples
175
176
### Basic DRF Setup
177
178
```python
179
# settings.py
180
REST_FRAMEWORK = {
181
'DEFAULT_AUTHENTICATION_CLASSES': [
182
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
183
'rest_framework.authentication.SessionAuthentication', # Optional: session auth
184
],
185
'DEFAULT_PERMISSION_CLASSES': [
186
'rest_framework.permissions.IsAuthenticated',
187
],
188
}
189
190
# Optional: OAuth2 settings
191
OAUTH2_PROVIDER = {
192
'SCOPES': {
193
'read': 'Read scope',
194
'write': 'Write scope',
195
'admin': 'Admin scope',
196
},
197
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600,
198
'ERROR_RESPONSE_WITH_SCOPES': True, # Include required scopes in error responses
199
}
200
```
201
202
### Function-Based Views
203
204
```python
205
from rest_framework.decorators import api_view, permission_classes
206
from rest_framework.response import Response
207
from oauth2_provider.contrib.rest_framework import TokenHasScope
208
209
@api_view(['GET'])
210
@permission_classes([TokenHasScope])
211
def read_only_api(request):
212
"""API endpoint requiring 'read' scope"""
213
required_scopes = ['read'] # Set on view function
214
return Response({'data': 'Read-only content'})
215
216
@api_view(['POST'])
217
@permission_classes([TokenHasScope])
218
def write_api(request):
219
"""API endpoint requiring 'write' scope"""
220
required_scopes = ['write']
221
return Response({'message': 'Data created'})
222
223
@api_view(['GET', 'POST'])
224
@permission_classes([TokenHasReadWriteScope])
225
def auto_scope_api(request):
226
"""API with automatic read/write scope assignment"""
227
if request.method == 'GET':
228
return Response({'data': 'Retrieved'})
229
else:
230
return Response({'message': 'Created'})
231
```
232
233
### Class-Based Views
234
235
```python
236
from rest_framework.views import APIView
237
from rest_framework.response import Response
238
from oauth2_provider.contrib.rest_framework import (
239
OAuth2Authentication,
240
TokenHasScope,
241
TokenHasReadWriteScope,
242
IsAuthenticatedOrTokenHasScope
243
)
244
245
class ProtectedAPIView(APIView):
246
"""Basic OAuth2 protected API view"""
247
authentication_classes = [OAuth2Authentication]
248
permission_classes = [TokenHasScope]
249
required_scopes = ['api']
250
251
def get(self, request):
252
return Response({
253
'user': request.user.username,
254
'scopes': request.auth.scope.split() if request.auth else []
255
})
256
257
class ReadWriteAPIView(APIView):
258
"""API view with read/write scope handling"""
259
authentication_classes = [OAuth2Authentication]
260
permission_classes = [TokenHasReadWriteScope]
261
262
def get(self, request):
263
# Requires 'read' scope
264
return Response({'data': 'content'})
265
266
def post(self, request):
267
# Requires 'write' scope
268
return Response({'message': 'created'})
269
270
class HybridAuthView(APIView):
271
"""View supporting both session and OAuth2 authentication"""
272
authentication_classes = [OAuth2Authentication, SessionAuthentication]
273
permission_classes = [IsAuthenticatedOrTokenHasScope]
274
required_scopes = ['api']
275
276
def get(self, request):
277
auth_type = 'oauth2' if hasattr(request, 'auth') and request.auth else 'session'
278
return Response({
279
'user': request.user.username,
280
'auth_type': auth_type
281
})
282
```
283
284
### ViewSets and Routers
285
286
```python
287
from rest_framework.viewsets import ModelViewSet
288
from rest_framework.routers import DefaultRouter
289
from oauth2_provider.contrib.rest_framework import OAuth2Authentication, TokenHasScope
290
291
class BookViewSet(ModelViewSet):
292
"""ViewSet with OAuth2 authentication and scope permissions"""
293
queryset = Book.objects.all()
294
serializer_class = BookSerializer
295
authentication_classes = [OAuth2Authentication]
296
permission_classes = [TokenHasScope]
297
required_scopes = ['books']
298
299
def get_permissions(self):
300
"""Custom permission logic based on action"""
301
if self.action in ['list', 'retrieve']:
302
# Read operations require 'read' scope
303
self.required_scopes = ['books:read']
304
elif self.action in ['create', 'update', 'partial_update', 'destroy']:
305
# Write operations require 'write' scope
306
self.required_scopes = ['books:write']
307
return super().get_permissions()
308
309
# URL configuration
310
router = DefaultRouter()
311
router.register(r'books', BookViewSet)
312
urlpatterns = router.urls
313
```
314
315
### Custom Permission Classes
316
317
```python
318
from oauth2_provider.contrib.rest_framework import TokenHasScope
319
320
class AdminOrTokenHasScope(TokenHasScope):
321
"""Permission allowing admin users or specific token scopes"""
322
323
def has_permission(self, request, view):
324
# Allow admin users regardless of token
325
if request.user.is_authenticated and request.user.is_staff:
326
return True
327
328
# Otherwise check token scopes
329
return super().has_permission(request, view)
330
331
class ResourceOwnerOrTokenHasScope(TokenHasScope):
332
"""Permission for resource owners or token with scopes"""
333
334
def has_object_permission(self, request, view, obj):
335
# Resource owner can always access their own objects
336
if hasattr(obj, 'user') and obj.user == request.user:
337
return True
338
339
# Otherwise check token scopes
340
return super().has_permission(request, view)
341
342
class ConditionalTokenScope(TokenHasScope):
343
"""Conditional scope requirements based on request context"""
344
345
def get_scopes(self, request, view):
346
"""Dynamic scope requirements"""
347
scopes = super().get_scopes(request, view)
348
349
# Add conditional scopes
350
if request.query_params.get('include_sensitive'):
351
scopes.append('sensitive_data')
352
353
if request.method in ['PUT', 'PATCH', 'DELETE']:
354
scopes.append('modify')
355
356
return scopes
357
```
358
359
### Error Handling and Responses
360
361
```python
362
from rest_framework.views import APIView
363
from rest_framework.response import Response
364
from rest_framework import status
365
from oauth2_provider.contrib.rest_framework import OAuth2Authentication, TokenHasScope
366
367
class ErrorHandlingView(APIView):
368
"""Demonstrates OAuth2 error handling in DRF"""
369
authentication_classes = [OAuth2Authentication]
370
permission_classes = [TokenHasScope]
371
required_scopes = ['read']
372
373
def get(self, request):
374
# Check if user is authenticated via OAuth2
375
if not hasattr(request, 'auth') or not request.auth:
376
return Response(
377
{'error': 'OAuth2 authentication required'},
378
status=status.HTTP_401_UNAUTHORIZED
379
)
380
381
# Check token validity
382
if request.auth.is_expired():
383
return Response(
384
{'error': 'Token has expired'},
385
status=status.HTTP_401_UNAUTHORIZED
386
)
387
388
# Check specific scopes manually
389
if not request.auth.allow_scopes(['read', 'special']):
390
return Response(
391
{
392
'error': 'Insufficient scopes',
393
'required_scopes': ['read', 'special'],
394
'provided_scopes': request.auth.scope.split()
395
},
396
status=status.HTTP_403_FORBIDDEN
397
)
398
399
return Response({'message': 'Access granted'})
400
401
# OAuth2 authentication errors are automatically handled:
402
# - No token: HTTP 401 with WWW-Authenticate header
403
# - Invalid token: HTTP 401 Unauthorized
404
# - Insufficient scopes: HTTP 403 Forbidden (with scope details if configured)
405
```
406
407
### Integration with API Documentation
408
409
```python
410
from rest_framework.views import APIView
411
from rest_framework.response import Response
412
from drf_yasg.utils import swagger_auto_schema
413
from drf_yasg import openapi
414
from oauth2_provider.contrib.rest_framework import TokenHasScope
415
416
class DocumentedAPIView(APIView):
417
"""API view with OAuth2 documentation for swagger/openapi"""
418
authentication_classes = [OAuth2Authentication]
419
permission_classes = [TokenHasScope]
420
required_scopes = ['read']
421
422
@swagger_auto_schema(
423
operation_description="Get protected data",
424
security=[{'oauth2': ['read']}],
425
responses={
426
200: openapi.Response('Success', examples={
427
'application/json': {'data': 'content'}
428
}),
429
401: openapi.Response('Unauthorized'),
430
403: openapi.Response('Forbidden - insufficient scopes')
431
}
432
)
433
def get(self, request):
434
return Response({'data': 'protected content'})
435
436
# settings.py for drf-yasg OAuth2 configuration
437
SWAGGER_SETTINGS = {
438
'SECURITY_DEFINITIONS': {
439
'oauth2': {
440
'type': 'oauth2',
441
'flow': 'authorization_code',
442
'authorizationUrl': 'https://example.com/o/authorize/',
443
'tokenUrl': 'https://example.com/o/token/',
444
'scopes': {
445
'read': 'Read access',
446
'write': 'Write access',
447
'admin': 'Admin access'
448
}
449
}
450
}
451
}
452
```
453
454
### Token Introspection in Views
455
456
```python
457
from rest_framework.views import APIView
458
from rest_framework.response import Response
459
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
460
461
class TokenInfoView(APIView):
462
"""View to inspect current OAuth2 token details"""
463
authentication_classes = [OAuth2Authentication]
464
465
def get(self, request):
466
if not hasattr(request, 'auth') or not request.auth:
467
return Response({'error': 'No OAuth2 token provided'}, status=401)
468
469
token = request.auth
470
return Response({
471
'token_info': {
472
'scopes': token.scope.split(),
473
'application': token.application.name,
474
'expires_at': token.expires.isoformat(),
475
'is_expired': token.is_expired(),
476
'user': token.user.username if token.user else None,
477
},
478
'user_info': {
479
'id': request.user.id,
480
'username': request.user.username,
481
'email': request.user.email,
482
'is_staff': request.user.is_staff,
483
}
484
})
485
```
486
487
### Throttling with OAuth2
488
489
```python
490
from rest_framework.throttling import UserRateThrottle
491
from rest_framework.views import APIView
492
from oauth2_provider.contrib.rest_framework import OAuth2Authentication, TokenHasScope
493
494
class OAuth2UserRateThrottle(UserRateThrottle):
495
"""Custom throttle that works with OAuth2 authentication"""
496
497
def get_cache_key(self, request, view):
498
# Use OAuth2 user if available, fallback to session user
499
if hasattr(request, 'auth') and request.auth and request.auth.user:
500
user = request.auth.user
501
else:
502
user = request.user
503
504
if user.is_authenticated:
505
ident = user.pk
506
else:
507
ident = self.get_ident(request)
508
509
return self.cache_format % {
510
'scope': self.scope,
511
'ident': ident
512
}
513
514
class ThrottledAPIView(APIView):
515
"""API view with OAuth2 authentication and throttling"""
516
authentication_classes = [OAuth2Authentication]
517
permission_classes = [TokenHasScope]
518
throttle_classes = [OAuth2UserRateThrottle]
519
throttle_scope = 'user'
520
required_scopes = ['api']
521
522
def get(self, request):
523
return Response({'message': 'throttled endpoint'})
524
525
# settings.py
526
REST_FRAMEWORK = {
527
'DEFAULT_THROTTLE_RATES': {
528
'user': '100/hour',
529
'anon': '10/hour',
530
}
531
}
532
```