or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

common-utilities.mddjango-integration.mdflask-integration.mdhttp-clients.mdindex.mdjose.mdoauth1.mdoauth2.mdoidc.md

django-integration.mddocs/

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

```