or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

drf-integration.mdindex.mdmanagement-commands.mdmanagement-views.mdmodels.mdoauth2-endpoints.mdoidc.mdsettings.mdview-protection.md

drf-integration.mddocs/

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

```