Protect your Django API with RBAC and JWT validation
This guide will help you implement authorization to secure your Django APIs using Role-based access control (RBAC) and JSON Web Tokens (JWTs) issued by Logto.
Before you start
Your client applications need to obtain access tokens from Logto. If you haven't set up client integration yet, check out our Quick starts for React, Vue, Angular, or other client frameworks, or see our Machine-to-machine guide for server-to-server access.
This guide focuses on the server-side validation of those tokens in your Django application.

What you will learn
- JWT validation: Learn to validate access tokens and extract authentication information
- Middleware implementation: Create reusable middleware for API protection
- Permission models: Understand and implement different authorization patterns:
- Global API resources for application-wide endpoints
- Organization permissions for tenant-specific feature control
- Organization-level API resources for multi-tenant data access
- RBAC integration: Enforce role-based permissions and scopes in your API endpoints
Prerequisites
- Latest stable version of Python installed
- Basic understanding of Django and web API development
- A Logto application configured (see Quick starts if needed)
Permission models overview
Before implementing protection, choose the permission model that fits your application architecture. This aligns with Logto's three main authorization scenarios:
- Global API resources
- Organization (non-API) permissions
- Organization-level API resources

- Use case: Protect API resources shared across your entire application (not organization-specific)
- Token type: Access token with global audience
- Examples: Public APIs, core product services, admin endpoints
- Best for: SaaS products with APIs used by all customers, microservices without tenant isolation
- Learn more: Protect global API resources

- Use case: Control organization-specific actions, UI features, or business logic (not APIs)
- Token type: Organization token with organization-specific audience
- Examples: Feature gating, dashboard permissions, member invitation controls
- Best for: Multi-tenant SaaS with organization-specific features and workflows
- Learn more: Protect organization (non-API) permissions

- Use case: Protect API resources accessible within a specific organization context
- Token type: Organization token with API resource audience + organization context
- Examples: Multi-tenant APIs, organization-scoped data endpoints, tenant-specific microservices
- Best for: Multi-tenant SaaS where API data is organization-scoped
- Learn more: Protect organization-level API resources
💡 Choose your model before proceeding - the implementation will reference your chosen approach throughout this guide.
Quick preparation steps
Configure Logto resources & permissions
- Global API resources
- Organization (non-API) permissions
- Organization-level API resources
- Create API resource: Go to Console → API resources and register your API (e.g.,
https://api.yourapp.com
) - Define permissions: Add scopes like
read:products
,write:orders
– see Define API resources with permissions - Create global roles: Go to Console → Roles and create roles that include your API permissions – see Configure global roles
- Assign roles: Assign roles to users or M2M applications that need API access
- Define organization permissions: Create non-API organization permissions like
invite:member
,manage:billing
in the organization template - Set up organization roles: Configure the organization template with organization-specific roles and assign permissions to them
- Assign organization roles: Assign users to organization roles within each organization context
- Create API resource: Register your API resource as above, but it will be used in organization context
- Define permissions: Add scopes like
read:data
,write:settings
that are scoped to organization context - Configure organization template: Set up organization roles that include your API resource permissions
- Assign organization roles: Assign users or M2M applications to organization roles that include API permissions
- Multi-tenant setup: Ensure your API can handle organization-scoped data and validation
Start with our Role-based access control guide for step-by-step setup instructions.
Update your client application
Request appropriate scopes in your client:
- User authentication: Update your app → to request your API scopes and/or organization context
- Machine-to-machine: Configure M2M scopes → for server-to-server access
The process usually involves updating your client configuration to include one or more of the following:
scope
parameter in OAuth flowsresource
parameter for API resource accessorganization_id
for organization context
Make sure the user or M2M app you are testing has been assigned proper roles or organization roles that include the necessary permissions for your API.
Initialize your API project
To initialize a new Django project, you can use Django's built-in commands:
django-admin startproject your_api_name
cd your_api_name
Install Django if you haven't already:
pip install Django
Create a basic Django app:
python manage.py startapp api
Create a basic API view:
from django.http import JsonResponse
def hello_view(request):
return JsonResponse({"message": "Hello from Django"})
Add URL configuration:
from django.urls import path
from . import views
urlpatterns = [
path('', views.hello_view, name='hello'),
]
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]
Start the development server:
python manage.py runserver
Refer to the Django documentation for more details on how to set up models, views, and other features.
Initialize constants and utilities
Define necessary constants and utilities in your code to handle token extraction and validation. A valid request must include an Authorization
header in the form Bearer <access_token>
.
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []
def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}
class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)
def extract_bearer_token_from_headers(headers: dict) -> str:
"""
Extract bearer token from HTTP headers.
Note: FastAPI and Django REST Framework have built-in token extraction,
so this function is primarily for Flask and other frameworks.
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('Authorization header is missing', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('Authorization header must start with "Bearer "', 401)
return authorization[7:] # Remove 'Bearer ' prefix
Retrieve info about your Logto tenant
You’ll need the following values to validate Logto-issued tokens:
- JSON Web Key Set (JWKS) URI: The URL to Logto’s public keys, used to verify JWT signatures.
- Issuer: The expected issuer value (Logto’s OIDC URL).
First, find your Logto tenant’s endpoint. You can find it in various places:
- In the Logto Console, under Settings → Domains.
- In any application settings where you configured in Logto, Settings → Endpoints & Credentials.
Fetch from OpenID Connect discovery endpoint
These values can be retrieved from Logto’s OpenID Connect discovery endpoint:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
Here’s an example response (other fields omitted for brevity):
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
Hardcode in your code (not recommended)
Since Logto doesn't allow customizing the JWKS URI or issuer, you can hardcode these values in your code. However, this is not recommended for production applications as it may increase maintenance overhead if some configuration changes in the future.
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- Issuer:
https://<your-logto-endpoint>/oidc
Validate the token and permissions
After extracting the token and fetching the OIDC config, validate the following:
- Signature: JWT must be valid and signed by Logto (via JWKS).
- Issuer: Must match your Logto tenant’s issuer.
- Audience: Must match the API’s resource indicator registered in Logto, or the organization context if applicable.
- Expiration: Token must not be expired.
- Permissions (scopes): Token must include required scopes for your API/action. Scopes are space-separated strings in the
scope
claim. - Organization context: If protecting organization-level API resources, validate the
organization_id
claim.
See JSON Web Token to learn more about JWT structure and claims.
What to check for each permission model
The claims and validation rules differ by permission model:
- Global API resources
- Organization (non-API) permissions
- Organization-level API resources
- Audience claim (
aud
): API resource indicator - Organization claim (
organization_id
): Not present - Scopes (permissions) to check (
scope
): API resource permissions
- Audience claim (
aud
):urn:logto:organization:<id>
(organization context is inaud
claim) - Organization claim (
organization_id
): Not present - Scopes (permissions) to check (
scope
): Organization permissions
- Audience claim (
aud
): API resource indicator - Organization claim (
organization_id
): Organization ID (must match request) - Scopes (permissions) to check (
scope
): API resource permissions
For non-API organization permissions, the organization context is represented by the aud
claim
(e.g., urn:logto:organization:abc123
). The organization_id
claim is only present for
organization-level API resource tokens.
Always validate both permissions (scopes) and context (audience, organization) for secure multi-tenant APIs.
Add the validation logic
We use PyJWT to validate JWTs. Install it if you haven't already:
pip install pyjwt[crypto]
First, add these shared utilities to handle JWT validation:
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt(token: str) -> Dict[str, Any]:
"""Validate JWT and return payload"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # We'll verify audience manually
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Invalid token: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'Token validation failed: {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""Create AuthInfo from JWT payload"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]
return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verify payload based on permission model"""
# Implement your verification logic here based on permission model
# This will be shown in the permission models section below
pass
Then, implement the middleware to verify the access token:
from django.http import JsonResponse
from jwt_validator import validate_jwt, create_auth_info
def require_access_token(view_func):
def wrapper(request, *args, **kwargs):
try:
headers = {key.replace('HTTP_', '').replace('_', '-').lower(): value
for key, value in request.META.items() if key.startswith('HTTP_')}
token = extract_bearer_token_from_headers(headers)
payload = validate_jwt(token)
# Attach auth info to request for generic use
request.auth = create_auth_info(payload)
return view_func(request, *args, **kwargs)
except AuthorizationError as e:
return JsonResponse({'error': str(e)}, status=e.status)
return wrapper
According to your permission model, implement the appropriate verification logic in jwt_validator.py
:
- Global API resources
- Organization (non-API) permissions
- Organization-level API resources
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verify payload for global API resources"""
# Check audience claim matches your API resource indicator
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience')
# Check required scopes for global API resources
required_scopes = ['api:read', 'api:write'] # Replace with your actual required scopes
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verify payload for organization permissions"""
# Check audience claim matches organization format
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
has_org_audience = any(aud.startswith('urn:logto:organization:') for aud in audiences)
if not has_org_audience:
raise AuthorizationError('Invalid audience for organization permissions')
# Check organization ID matches the context (you may need to extract this from request context)
expected_org_id = 'your-organization-id' # Extract from request context
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('Organization ID mismatch')
# Check required organization scopes
required_scopes = ['invite:users', 'manage:settings'] # Replace with your actual required scopes
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Verify payload for organization-level API resources"""
# Check audience claim matches your API resource indicator
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience for organization-level API resources')
# Check organization ID matches the context (you may need to extract this from request context)
expected_org_id = 'your-organization-id' # Extract from request context
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('Organization ID mismatch')
# Check required scopes for organization-level API resources
required_scopes = ['api:read', 'api:write'] # Replace with your actual required scopes
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization-level API scopes')
Apply the middleware to your API
Now, apply the middleware to your protected API routes.
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Access auth information from request.auth
return JsonResponse({"auth": request.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
]
Test your protected API
Get access tokens
From your client application: If you've set up a client integration, your app can obtain tokens automatically. Extract the access token and use it in API requests.
For testing with curl/Postman:
-
User tokens: Use your client app's developer tools to copy the access token from localStorage or the network tab
-
Machine-to-machine tokens: Use the client credentials flow. Here's a non-normative example using curl:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"You may need to adjust the
resource
andscope
parameters based on your API resource and permissions; anorganization_id
parameter may also be required if your API is organization-scoped.
Need to inspect the token contents? Use our JWT decoder to decode and verify your JWTs.
Test protected endpoints
Valid token request
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
Expected response:
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
Missing token
curl http://localhost:3000/api/protected
Expected response (401):
{
"error": "Authorization header is missing"
}
Invalid token
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
Expected response (401):
{
"error": "Invalid token"
}
Permission model-specific testing
- Global API resources
- Organization (non-API) permissions
- Organization-level API resources
Test scenarios for APIs protected with global scopes:
- Valid scopes: Test with tokens that include your required API scopes (e.g.,
api:read
,api:write
) - Missing scopes: Expect 403 Forbidden when token lacks required scopes
- Wrong audience: Expect 403 Forbidden when audience does not match the API resource
# Token with missing scopes - expect 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
Test scenarios for organization-specific access control:
- Valid organization token: Test with tokens that include correct organization context (organization ID and scopes)
- Missing scopes: Expect 403 Forbidden when user doesn't have permissions for the requested action
- Wrong organization: Expect 403 Forbidden when audience does not match the organization context (
urn:logto:organization:<organization_id>
)
# Token for wrong organization - expect 403
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
Test scenarios combining API resource validation with organization context:
- Valid organization + API scopes: Test with tokens that have both organization context and required API scopes
- Missing API scopes: Expect 403 Forbidden when organization token lacks required API permissions
- Wrong organization: Expect 403 Forbidden when accessing API with token from different organization
- Wrong audience: Expect 403 Forbidden when audience does not match the organization-level API resource
# Organization token without API scopes - expect 403
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
Further reading
RBAC in practice: Implementing secure authorization for your application
Build a multi-tenant SaaS application: A complete guide from design to implementation