0
# Django Integration
1
2
Django-specific OAuth 2.0 server implementation with seamless Django model integration, middleware support, and Django-specific request/response handling. Provides comprehensive support for Django's ORM, authentication system, and HTTP request/response cycle.
3
4
## Capabilities
5
6
### Django OAuth 2.0 Server
7
8
Django-specific OAuth 2.0 authorization server implementation optimized for Django applications.
9
10
```python { .api }
11
class AuthorizationServer:
12
"""Django OAuth 2.0 authorization server."""
13
14
def __init__(self, query_client: callable, save_token: callable) -> None:
15
"""
16
Initialize Django authorization server.
17
18
Args:
19
query_client: Function to query client by client_id
20
save_token: Function to save issued tokens
21
"""
22
23
def register_grant(self, grant_cls: type, extensions: list = None) -> None:
24
"""
25
Register a grant type.
26
27
Args:
28
grant_cls: Grant class to register
29
extensions: List of grant extensions
30
"""
31
32
def register_endpoint(self, endpoint: object) -> None:
33
"""
34
Register an endpoint.
35
36
Args:
37
endpoint: Endpoint instance
38
"""
39
40
def create_authorization_response(self, request: HttpRequest, grant_user: callable = None) -> HttpResponse:
41
"""
42
Create authorization response for Django.
43
44
Args:
45
request: Django HttpRequest object
46
grant_user: Function to grant authorization to user
47
48
Returns:
49
Django HttpResponse object
50
"""
51
52
def create_token_response(self, request: HttpRequest) -> HttpResponse:
53
"""
54
Create token response for Django.
55
56
Args:
57
request: Django HttpRequest object
58
59
Returns:
60
Django HttpResponse object
61
"""
62
63
def create_revocation_response(self, request: HttpRequest) -> HttpResponse:
64
"""
65
Create revocation response for Django.
66
67
Args:
68
request: Django HttpRequest object
69
70
Returns:
71
Django HttpResponse object
72
"""
73
74
def create_introspection_response(self, request: HttpRequest) -> HttpResponse:
75
"""
76
Create introspection response for Django.
77
78
Args:
79
request: Django HttpRequest object
80
81
Returns:
82
Django HttpResponse object
83
"""
84
85
def validate_consent_request(self, request: HttpRequest, end_user=None) -> None:
86
"""
87
Validate consent request at authorization endpoint.
88
89
Args:
90
request: Django HttpRequest object
91
end_user: End user object (Django User model)
92
"""
93
```
94
95
### Django Resource Protection
96
97
Django-specific OAuth 2.0 resource server protection with Django model integration.
98
99
```python { .api }
100
class ResourceProtector:
101
"""Django OAuth 2.0 resource protector."""
102
103
def __init__(self, require_oauth: callable = None) -> None:
104
"""
105
Initialize Django resource protector.
106
107
Args:
108
require_oauth: Optional default OAuth requirement function
109
"""
110
111
def register_token_validator(self, validator: 'BearerTokenValidator') -> None:
112
"""
113
Register bearer token validator.
114
115
Args:
116
validator: Token validator instance
117
"""
118
119
def __call__(self, scopes: list = None, optional: bool = False) -> callable:
120
"""
121
Decorator for protecting Django views.
122
123
Args:
124
scopes: Required scopes
125
optional: Whether protection is optional
126
127
Returns:
128
View decorator function
129
"""
130
131
def acquire_token(self, request: HttpRequest, scopes: list = None, raise_error: bool = True) -> 'OAuth2Token':
132
"""
133
Acquire token from Django request.
134
135
Args:
136
request: Django HttpRequest object
137
scopes: Required scopes
138
raise_error: Whether to raise error if token invalid
139
140
Returns:
141
OAuth2Token object if valid
142
"""
143
144
def validate_request(self, request: HttpRequest, scopes: list = None) -> 'OAuth2Token':
145
"""
146
Validate OAuth 2.0 request.
147
148
Args:
149
request: Django HttpRequest object
150
scopes: Required scopes
151
152
Returns:
153
OAuth2Token object if valid
154
"""
155
```
156
157
### Django Bearer Token Validator
158
159
Django-specific bearer token validator with ORM integration.
160
161
```python { .api }
162
class BearerTokenValidator:
163
"""Django bearer token validator."""
164
165
def __init__(self, token_model: type = None, realm: str = None) -> None:
166
"""
167
Initialize Django bearer token validator.
168
169
Args:
170
token_model: Django model class for tokens
171
realm: OAuth realm for WWW-Authenticate header
172
"""
173
174
def authenticate_token(self, token_string: str) -> object:
175
"""
176
Authenticate bearer token using Django ORM.
177
178
Args:
179
token_string: Bearer token string
180
181
Returns:
182
Token model instance if valid
183
"""
184
185
def request_invalid(self, request: HttpRequest) -> bool:
186
"""
187
Check if Django request is invalid.
188
189
Args:
190
request: Django HttpRequest object
191
192
Returns:
193
True if request is invalid
194
"""
195
196
def token_revoked(self, token: object) -> bool:
197
"""
198
Check if token is revoked.
199
200
Args:
201
token: Token model instance
202
203
Returns:
204
True if token is revoked
205
"""
206
207
def token_expired(self, token: object) -> bool:
208
"""
209
Check if token is expired.
210
211
Args:
212
token: Token model instance
213
214
Returns:
215
True if token is expired
216
"""
217
218
def get_token_scopes(self, token: object) -> list:
219
"""
220
Get token scopes from Django model.
221
222
Args:
223
token: Token model instance
224
225
Returns:
226
List of scope strings
227
"""
228
```
229
230
### Django Revocation Endpoint
231
232
OAuth 2.0 token revocation endpoint for Django.
233
234
```python { .api }
235
class RevocationEndpoint:
236
"""Django OAuth 2.0 revocation endpoint."""
237
238
def __init__(self, query_token: callable, revoke_token: callable) -> None:
239
"""
240
Initialize Django revocation endpoint.
241
242
Args:
243
query_token: Function to query token by value
244
revoke_token: Function to revoke token
245
"""
246
247
def create_revocation_response(self, request: HttpRequest) -> HttpResponse:
248
"""
249
Create revocation response for Django.
250
251
Args:
252
request: Django HttpRequest object
253
254
Returns:
255
Django HttpResponse object
256
"""
257
258
def query_token(self, token: str, token_type_hint: str = None, client: object = None) -> object:
259
"""
260
Query token by value.
261
262
Args:
263
token: Token string
264
token_type_hint: Hint about token type
265
client: Client object
266
267
Returns:
268
Token object if found
269
"""
270
271
def revoke_token(self, token: object, client: object = None) -> None:
272
"""
273
Revoke token.
274
275
Args:
276
token: Token object to revoke
277
client: Client object
278
"""
279
```
280
281
### Django Model Mixins
282
283
Enhanced model mixins for Django ORM integration with OAuth 2.0.
284
285
```python { .api }
286
class ClientMixin:
287
"""Mixin for Django OAuth 2.0 client model."""
288
289
client_id: str # Client identifier field
290
client_secret: str # Client secret field (may be None)
291
client_id_issued_at: int # Client ID issued timestamp
292
client_secret_expires_at: int # Client secret expiration
293
294
def get_client_id(self) -> str:
295
"""Get client ID."""
296
return self.client_id
297
298
def get_default_redirect_uri(self) -> str:
299
"""Get default redirect URI for this client."""
300
return getattr(self, 'default_redirect_uri', '')
301
302
def get_allowed_scope(self, scope: str) -> str:
303
"""
304
Get allowed scope for client.
305
306
Args:
307
scope: Requested scope
308
309
Returns:
310
Allowed scope string
311
"""
312
allowed = getattr(self, 'allowed_scopes', '')
313
if not scope:
314
return allowed
315
scopes = scope.split()
316
allowed_scopes = allowed.split()
317
return ' '.join([s for s in scopes if s in allowed_scopes])
318
319
def check_redirect_uri(self, redirect_uri: str) -> bool:
320
"""
321
Check if redirect URI is allowed for this client.
322
323
Args:
324
redirect_uri: Redirect URI to check
325
326
Returns:
327
True if redirect URI is allowed
328
"""
329
return redirect_uri in self.get_allowed_redirect_uris()
330
331
def has_client_secret(self) -> bool:
332
"""Check if client has a secret."""
333
return bool(self.client_secret)
334
335
def check_client_secret(self, client_secret: str) -> bool:
336
"""
337
Verify client secret.
338
339
Args:
340
client_secret: Secret to verify
341
342
Returns:
343
True if secret is valid
344
"""
345
return self.client_secret == client_secret
346
347
def check_token_endpoint_auth_method(self, method: str) -> bool:
348
"""
349
Check if token endpoint auth method is supported.
350
351
Args:
352
method: Authentication method
353
354
Returns:
355
True if method is supported
356
"""
357
return method in getattr(self, 'token_endpoint_auth_methods', ['client_secret_basic'])
358
359
def check_response_type(self, response_type: str) -> bool:
360
"""
361
Check if response type is supported.
362
363
Args:
364
response_type: Response type to check
365
366
Returns:
367
True if response type is supported
368
"""
369
return response_type in getattr(self, 'response_types', ['code'])
370
371
def check_grant_type(self, grant_type: str) -> bool:
372
"""
373
Check if grant type is supported.
374
375
Args:
376
grant_type: Grant type to check
377
378
Returns:
379
True if grant type is supported
380
"""
381
return grant_type in getattr(self, 'grant_types', ['authorization_code'])
382
383
class AuthorizationCodeMixin:
384
"""Mixin for Django authorization code model."""
385
386
code: str # Authorization code field
387
client_id: str # Client identifier field
388
redirect_uri: str # Redirect URI field
389
scope: str # Authorized scope field
390
user_id: str # User identifier field
391
code_challenge: str # PKCE code challenge field
392
code_challenge_method: str # PKCE challenge method field
393
394
def is_expired(self) -> bool:
395
"""
396
Check if authorization code is expired.
397
398
Returns:
399
True if code is expired
400
"""
401
from django.utils import timezone
402
expires_at = getattr(self, 'expires_at', None)
403
if not expires_at:
404
return False
405
return timezone.now() > expires_at
406
407
def get_redirect_uri(self) -> str:
408
"""Get redirect URI."""
409
return self.redirect_uri
410
411
def get_scope(self) -> str:
412
"""Get authorized scope."""
413
return self.scope
414
415
def get_user_id(self) -> str:
416
"""Get user ID."""
417
return str(self.user_id)
418
419
def get_code_challenge(self) -> str:
420
"""Get PKCE code challenge."""
421
return getattr(self, 'code_challenge', '')
422
423
def get_code_challenge_method(self) -> str:
424
"""Get PKCE challenge method."""
425
return getattr(self, 'code_challenge_method', '')
426
427
class TokenMixin:
428
"""Mixin for Django access token model."""
429
430
access_token: str # Access token field
431
client_id: str # Client identifier field
432
token_type: str # Token type field
433
refresh_token: str # Refresh token field
434
scope: str # Token scope field
435
user_id: str # User identifier field
436
issued_at: int # Token issued timestamp
437
expires_in: int # Token lifetime in seconds
438
439
def get_scope(self) -> str:
440
"""Get token scope."""
441
return self.scope or ''
442
443
def get_user_id(self) -> str:
444
"""Get user ID."""
445
return str(self.user_id)
446
447
def is_expired(self) -> bool:
448
"""
449
Check if token is expired.
450
451
Returns:
452
True if token is expired
453
"""
454
from django.utils import timezone
455
if not self.expires_in:
456
return False
457
expires_at = self.issued_at + self.expires_in
458
return timezone.now().timestamp() > expires_at
459
460
def is_revoked(self) -> bool:
461
"""
462
Check if token is revoked.
463
464
Returns:
465
True if token is revoked
466
"""
467
return getattr(self, 'revoked', False)
468
469
def get_expires_at(self) -> int:
470
"""
471
Get expiration timestamp.
472
473
Returns:
474
Expiration timestamp
475
"""
476
if not self.expires_in:
477
return 0
478
return self.issued_at + self.expires_in
479
```
480
481
### Django Signals
482
483
Django signals for OAuth 2.0 events.
484
485
```python { .api }
486
import django.dispatch
487
488
# OAuth 2.0 server signals
489
client_authenticated = django.dispatch.Signal() # providing_args=['client']
490
token_authenticated = django.dispatch.Signal() # providing_args=['token']
491
token_revoked = django.dispatch.Signal() # providing_args=['token']
492
```
493
494
## Usage Examples
495
496
### Django OAuth 2.0 Server Setup
497
498
```python
499
# models.py
500
from django.db import models
501
from django.contrib.auth.models import User
502
from authlib.integrations.django_oauth2 import ClientMixin, AuthorizationCodeMixin, TokenMixin
503
504
class Client(models.Model, ClientMixin):
505
client_id = models.CharField(max_length=40, unique=True)
506
client_secret = models.CharField(max_length=55, blank=True)
507
name = models.CharField(max_length=100)
508
redirect_uris = models.TextField()
509
allowed_scopes = models.TextField(default='')
510
response_types = models.TextField(default='code')
511
grant_types = models.TextField(default='authorization_code refresh_token')
512
513
def get_allowed_redirect_uris(self):
514
return self.redirect_uris.split()
515
516
class AuthorizationCode(models.Model, AuthorizationCodeMixin):
517
user = models.ForeignKey(User, on_delete=models.CASCADE)
518
client = models.ForeignKey(Client, on_delete=models.CASCADE)
519
code = models.CharField(max_length=120, unique=True)
520
redirect_uri = models.TextField()
521
scope = models.TextField(default='')
522
created_at = models.DateTimeField(auto_now_add=True)
523
expires_at = models.DateTimeField()
524
code_challenge = models.TextField(blank=True)
525
code_challenge_method = models.CharField(max_length=10, blank=True)
526
527
@property
528
def client_id(self):
529
return self.client.client_id
530
531
@property
532
def user_id(self):
533
return self.user.id
534
535
class Token(models.Model, TokenMixin):
536
user = models.ForeignKey(User, on_delete=models.CASCADE)
537
client = models.ForeignKey(Client, on_delete=models.CASCADE)
538
access_token = models.CharField(max_length=255, unique=True)
539
refresh_token = models.CharField(max_length=255, unique=True, blank=True)
540
token_type = models.CharField(max_length=20, default='Bearer')
541
scope = models.TextField(default='')
542
created_at = models.DateTimeField(auto_now_add=True)
543
expires_in = models.IntegerField(default=3600)
544
revoked = models.BooleanField(default=False)
545
546
@property
547
def client_id(self):
548
return self.client.client_id
549
550
@property
551
def user_id(self):
552
return self.user.id
553
554
@property
555
def issued_at(self):
556
return int(self.created_at.timestamp())
557
558
# views.py
559
from django.http import HttpResponse
560
from django.shortcuts import render, redirect
561
from django.contrib.auth.decorators import login_required
562
from django.views.decorators.csrf import csrf_exempt
563
from authlib.integrations.django_oauth2 import AuthorizationServer, ResourceProtector
564
from authlib.oauth2.rfc6749.grants import AuthorizationCodeGrant, RefreshTokenGrant
565
566
def query_client(client_id):
567
try:
568
return Client.objects.get(client_id=client_id)
569
except Client.DoesNotExist:
570
return None
571
572
def save_token(token, request, *args, **kwargs):
573
if request.grant_type == 'authorization_code':
574
code = request.credential
575
Token.objects.create(
576
client=request.client,
577
user_id=code.user_id,
578
access_token=token['access_token'],
579
refresh_token=token.get('refresh_token', ''),
580
scope=token.get('scope', ''),
581
expires_in=token.get('expires_in', 3600)
582
)
583
584
# Initialize authorization server
585
authorization_server = AuthorizationServer(
586
query_client=query_client,
587
save_token=save_token
588
)
589
authorization_server.register_grant(AuthorizationCodeGrant)
590
authorization_server.register_grant(RefreshTokenGrant)
591
592
@login_required
593
def authorize(request):
594
if request.method == 'GET':
595
try:
596
grant = authorization_server.validate_consent_request(request, end_user=request.user)
597
return render(request, 'oauth2/authorize.html', {
598
'grant': grant,
599
'user': request.user
600
})
601
except OAuth2Error as error:
602
return HttpResponse(f'Error: {error.error}', status=400)
603
604
if request.POST.get('confirm'):
605
grant_user = request.user
606
else:
607
grant_user = None
608
609
return authorization_server.create_authorization_response(request, grant_user)
610
611
@csrf_exempt
612
def issue_token(request):
613
return authorization_server.create_token_response(request)
614
615
@csrf_exempt
616
def revoke_token(request):
617
return authorization_server.create_revocation_response(request)
618
```
619
620
### Resource Protection
621
622
```python
623
# api/views.py
624
from django.http import JsonResponse
625
from django.contrib.auth.models import User
626
from authlib.integrations.django_oauth2 import ResourceProtector, BearerTokenValidator
627
628
class MyBearerTokenValidator(BearerTokenValidator):
629
def authenticate_token(self, token_string):
630
try:
631
return Token.objects.get(access_token=token_string, revoked=False)
632
except Token.DoesNotExist:
633
return None
634
635
def token_expired(self, token):
636
return token.is_expired()
637
638
def get_token_scopes(self, token):
639
return token.scope.split() if token.scope else []
640
641
# Initialize resource protector
642
require_oauth = ResourceProtector()
643
require_oauth.register_token_validator(MyBearerTokenValidator())
644
645
@require_oauth('profile')
646
def api_user(request):
647
token = require_oauth.acquire_token(request)
648
user = User.objects.get(id=token.user_id)
649
return JsonResponse({
650
'id': user.id,
651
'username': user.username,
652
'email': user.email,
653
'first_name': user.first_name,
654
'last_name': user.last_name
655
})
656
657
@require_oauth('read')
658
def api_posts(request):
659
token = require_oauth.acquire_token(request)
660
# Get posts for authenticated user
661
posts = Post.objects.filter(author_id=token.user_id)
662
return JsonResponse({
663
'posts': [{
664
'id': post.id,
665
'title': post.title,
666
'content': post.content,
667
'created_at': post.created_at.isoformat()
668
} for post in posts]
669
})
670
671
@require_oauth('write')
672
def api_create_post(request):
673
token = require_oauth.acquire_token(request)
674
import json
675
data = json.loads(request.body)
676
677
post = Post.objects.create(
678
author_id=token.user_id,
679
title=data['title'],
680
content=data['content']
681
)
682
683
return JsonResponse({'id': post.id}, status=201)
684
685
# Optional protection
686
@require_oauth(optional=True)
687
def api_public_posts(request):
688
token = require_oauth.acquire_token(request, raise_error=False)
689
690
if token:
691
# Show personalized content for authenticated users
692
posts = Post.objects.filter(public=True).order_by('-created_at')
693
message = f'Hello {User.objects.get(id=token.user_id).username}'
694
else:
695
# Show limited content for anonymous users
696
posts = Post.objects.filter(public=True, featured=True).order_by('-created_at')
697
message = 'Hello anonymous user'
698
699
return JsonResponse({
700
'message': message,
701
'posts': [{'id': p.id, 'title': p.title} for p in posts[:10]]
702
})
703
```
704
705
### URL Configuration
706
707
```python
708
# urls.py
709
from django.urls import path
710
from . import views
711
712
urlpatterns = [
713
# OAuth 2.0 endpoints
714
path('authorize/', views.authorize, name='oauth2_authorize'),
715
path('token/', views.issue_token, name='oauth2_token'),
716
path('revoke/', views.revoke_token, name='oauth2_revoke'),
717
718
# API endpoints
719
path('api/user/', views.api_user, name='api_user'),
720
path('api/posts/', views.api_posts, name='api_posts'),
721
path('api/posts/create/', views.api_create_post, name='api_create_post'),
722
path('api/public/', views.api_public_posts, name='api_public_posts'),
723
]
724
```
725
726
### Using Django Signals
727
728
```python
729
# signals.py
730
from django.dispatch import receiver
731
from authlib.integrations.django_oauth2 import client_authenticated, token_authenticated, token_revoked
732
import logging
733
734
logger = logging.getLogger(__name__)
735
736
@receiver(client_authenticated)
737
def on_client_authenticated(sender, client=None, **kwargs):
738
logger.info(f'Client {client.client_id} authenticated')
739
# Update client statistics, log authentication, etc.
740
741
@receiver(token_authenticated)
742
def on_token_authenticated(sender, token=None, **kwargs):
743
logger.info(f'Token for user {token.user_id} authenticated')
744
# Update user last activity, log API usage, etc.
745
User.objects.filter(id=token.user_id).update(last_api_access=timezone.now())
746
747
@receiver(token_revoked)
748
def on_token_revoked(sender, token=None, **kwargs):
749
logger.info(f'Token {token.access_token} revoked')
750
# Clean up related resources, notify user, etc.
751
# Send push notification about token revocation
752
```
753
754
### Management Commands
755
756
```python
757
# management/commands/create_oauth_client.py
758
from django.core.management.base import BaseCommand
759
from myapp.models import Client
760
import secrets
761
762
class Command(BaseCommand):
763
help = 'Create OAuth 2.0 client'
764
765
def add_arguments(self, parser):
766
parser.add_argument('name', type=str, help='Client name')
767
parser.add_argument('--redirect-uris', required=True, help='Redirect URIs (space-separated)')
768
parser.add_argument('--scopes', default='read write', help='Allowed scopes')
769
770
def handle(self, *args, **options):
771
client = Client.objects.create(
772
client_id=secrets.token_urlsafe(32),
773
client_secret=secrets.token_urlsafe(48),
774
name=options['name'],
775
redirect_uris=options['redirect_uris'],
776
allowed_scopes=options['scopes']
777
)
778
779
self.stdout.write(self.style.SUCCESS(f'Created client: {client.client_id}'))
780
self.stdout.write(f'Client Secret: {client.client_secret}')
781
```
782
783
### Middleware Integration
784
785
```python
786
# middleware.py
787
class OAuthMiddleware:
788
def __init__(self, get_response):
789
self.get_response = get_response
790
791
def __call__(self, request):
792
# Add OAuth token to request if available
793
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
794
if auth_header.startswith('Bearer '):
795
token_string = auth_header[7:]
796
try:
797
token = Token.objects.get(access_token=token_string, revoked=False)
798
if not token.is_expired():
799
request.oauth_token = token
800
request.user = User.objects.get(id=token.user_id)
801
except Token.DoesNotExist:
802
pass
803
804
response = self.get_response(request)
805
return response
806
```