A convenient package for building both client and server implementations for JWTs or Json Web Tokens. It allows you to build either side by only including the module you need and includes an optional custom user model.
Project description
Django-EasyJWT
A Django package for implementing remote JWT authentication in microservice architectures. It provides a centralized authentication service with multiple client services authenticating against it.
Table of Contents
- Why Django-EasyJWT?
- Features
- Installation
- Architecture Overview
- Quick Start
- Configuration Reference
- API Endpoints
- Testing the API
- Advanced Usage
- Running Tests
- Changelog
- Acknowledgements
- License
Why Django-EasyJWT?
When managing multiple services with the same users, a centralized authentication service eliminates password confusion and provides a single source of truth. Django-EasyJWT was built for scenarios where:
- Multiple services share the same user base
- Different services require different access levels
- You need custom user data and permissions passed through authentication
- You want to keep your auth service lean and behind a private network
Features
- JWT Authentication: Access and refresh token support
- Remote Auth: Client services authenticate against a central auth service
- Session Support: Authenticated users can access Django admin
- Custom User Model: Optional email-based user model included
- Token Blacklisting: Optional token revocation support
- Extensible: Custom serializers for additional user data
- Configurable Timeouts: HTTP request timeouts and SSL verification
Installation
uv pip install django-easyjwt
Or with pip:
pip install django-easyjwt
Architecture Overview
┌─────────────────┐ ┌─────────────────┐
│ Client-Service │ ──────► │ Auth-Service │
│ (Port 8001) │ │ (Port 8000) │
├─────────────────┤ ├─────────────────┤
│ easyjwt_client │ │ easyjwt_auth │
│ easyjwt_user │ │ easyjwt_user │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
Local DB Auth DB
(User copies) (User source)
The package contains three sub-packages:
| Package | Used In | Purpose |
|---|---|---|
easyjwt_auth |
Auth-Service | JWT token generation and validation |
easyjwt_client |
Client-Service | Remote authentication against auth-service |
easyjwt_user |
Both (optional) | Custom user model with email as username |
Quick Start
Create an Auth-Service
uv venv
source .venv/bin/activate
uv pip install django djangorestframework django_easyjwt
uv run django-admin startproject config
mv config auth-service
cd auth-service
Add to config/settings.py:
from datetime import timedelta
INSTALLED_APPS = [
# ...
'rest_framework',
'easyjwt_auth',
'easyjwt_user',
]
AUTH_USER_MODEL = "easyjwt_user.User"
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"easyjwt_auth.authentication.JWTAuthentication",
),
}
EASY_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": "d577273ff885c3f84dadb8578bb40000", # Set properly for production!
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "easyjwt_auth.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("easyjwt_auth.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "easyjwt_auth.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
Add to config/urls.py:
from django.urls import path, include
urlpatterns = [
# ...
path('auth/', include('easyjwt_auth.urls')),
path('auth/', include('easyjwt_user.urls')),
]
Run migrations and create a superuser:
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py runserver 0.0.0.0:8000
Create a Client-Service
cd ..
uv venv
source .venv/bin/activate
uv pip install django django_rest_framework django_easyjwt
uv run django-admin startproject config
mv config client-service
cd client-service
Add to config/settings.py:
INSTALLED_APPS = [
# ...
'rest_framework',
'easyjwt_client',
'easyjwt_user',
]
AUTH_USER_MODEL = "easyjwt_user.User"
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_AUTHENTICATION_CLASSES": (
"easyjwt_client.authentication.EasyJWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
}
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'easyjwt_client.authentication.RemoteAuthBackend',
]
EASY_JWT = {
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "Authorization",
"REMOTE_AUTH_SERVICE_URL": "http://127.0.0.1:8000",
"REMOTE_AUTH_SERVICE_TOKEN_PATH": "/auth/token/",
"REMOTE_AUTH_SERVICE_REFRESH_PATH": "/auth/token/refresh/",
"REMOTE_AUTH_SERVICE_VERIFY_PATH": "/auth/token/verify/",
"REMOTE_AUTH_SERVICE_USER_PATH": "/auth/user/",
"REMOTE_AUTH_REQUEST_TIMEOUT": 30,
"REMOTE_AUTH_SSL_VERIFY": True,
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
Add to config/urls.py:
from django.urls import path, include
from test_app.views import TestView
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include("easyjwt_client.urls")),
path('api/test/', TestView.as_view()),
]
Create test_app/views.py:
from rest_framework import generics
from rest_framework.response import Response
class TestView(generics.GenericAPIView):
def get(self, request):
return Response("success", status=200)
Run migrations:
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py runserver 0.0.0.0:8001
Standing up the Services
Run both services in separate terminals:
# Terminal 1 - Auth-Service
cd auth-service
source .venv/bin/activate
uv run python manage.py runserver 0.0.0.0:8000
# Terminal 2 - Client-Service
cd client-service
source .venv/bin/activate
uv run python manage.py runserver 0.0.0.0:8001
Configuration Reference
Auth-Service Settings
| Setting | Type | Default | Description |
|---|---|---|---|
ACCESS_TOKEN_LIFETIME |
timedelta | Required | Access token validity duration |
REFRESH_TOKEN_LIFETIME |
timedelta | Required | Refresh token validity duration |
ALGORITHM |
str | "HS256" |
JWT signing algorithm |
SIGNING_KEY |
str | Required | Secret key for signing tokens |
USER_ID_FIELD |
str | "id" |
User model field for ID |
USER_ID_CLAIM |
str | "user_id" |
JWT claim for user ID |
CHECK_REVOKE_TOKEN |
bool | False |
Enable password-change token revocation |
Client-Service Settings
| Setting | Type | Default | Description |
|---|---|---|---|
REMOTE_AUTH_SERVICE_URL |
str | Required | Base URL of auth-service |
REMOTE_AUTH_SERVICE_TOKEN_PATH |
str | "/auth/token/" |
Token endpoint path |
REMOTE_AUTH_SERVICE_REFRESH_PATH |
str | "/auth/token/refresh/" |
Refresh endpoint path |
REMOTE_AUTH_SERVICE_VERIFY_PATH |
str | "/auth/token/verify/" |
Verify endpoint path |
REMOTE_AUTH_SERVICE_USER_PATH |
str | "/auth/user/" |
User endpoint path |
REMOTE_AUTH_REQUEST_TIMEOUT |
int | 30 |
HTTP request timeout in seconds |
REMOTE_AUTH_SSL_VERIFY |
bool | True |
Verify SSL certificates |
AUTH_HEADER_TYPES |
tuple | ("Bearer",) |
Valid auth header types |
USER_MODEL_SERIALIZER |
str | Built-in | Custom user serializer path |
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/auth/token/ |
POST | Obtain access and refresh tokens |
/auth/token/refresh/ |
POST | Refresh an expired access token |
/auth/token/verify/ |
POST | Verify if a token is valid |
/auth/users/ |
GET | List users (auth-service only) |
/auth/users/{id}/ |
GET | Get user details |
Testing the API
Obtain Token Pair
curl -X POST \
-H "Content-Type: application/json" \
-d '{"email": "user@test.com", "password": "user-pass"}' \
http://127.0.0.1:8001/auth/token/
Response:
{
"refresh": "...",
"access": "..."
}
Make Authenticated Request
export ACCESS_TOKEN=<your_access_token>
curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
http://127.0.0.1:8001/api/test/
Response: "success"
Refresh Token
curl -X POST \
-H "Content-Type: application/json" \
-d '{"refresh": "${REFRESH_TOKEN}"}' \
http://127.0.0.1:8001/auth/token/refresh/
Verify Token
curl -X POST \
-H "Content-Type: application/json" \
-d '{"token": "${ACCESS_TOKEN}"}' \
http://127.0.0.1:8001/auth/token/verify/
Advanced Usage
Extra Data
You can pass additional user data from the auth-service to client-services using custom serializers.
Auth-Service Configuration
models.py:
class AccessGroup(models.Model):
user = models.OneToOneField(User, related_name="accessgroup", on_delete=models.CASCADE)
user_type = models.TextField()
serializers.py:
class AccessGroupSerializer(serializers.ModelSerializer):
class Meta:
model = AccessGroup
fields = ("user_type",)
class TokenUserSerializer(serializers.ModelSerializer):
accessgroup = AccessGroupSerializer()
class Meta:
model = User
fields = ("id", "email", "first_name", "last_name", "accessgroup")
Add to EASY_JWT settings:
"USER_MODEL_SERIALIZER": "userdata.serializers.TokenUserSerializer",
Client-Service Configuration
Use the same model and serializer, but override create() and update() methods:
class TokenUserSerializer(serializers.ModelSerializer):
accessgroup = AccessGroupSerializer()
class Meta:
model = User
fields = ("id", "email", "first_name", "last_name", "accessgroup")
def create(self, validated_data):
accessgroup = validated_data.pop("accessgroup")
user, _ = User.objects.get_or_create(
email=validated_data.pop("email"),
defaults=validated_data
)
AccessGroup.objects.update_or_create(user=user, defaults=accessgroup)
return user
def update(self, instance, validated_data):
accessgroup = validated_data.pop("accessgroup")
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
AccessGroup.objects.update_or_create(user=instance, defaults=accessgroup)
return instance
Permissions
Use custom permission classes to control access based on user data:
from rest_framework import permissions
class AccessGroupPermission(permissions.BasePermission):
message = "You do not have permission to this service"
def has_permission(self, request, view):
return (
not request.user.is_anonymous
and hasattr(request.user, 'accessgroup')
and view.access_level.startswith(request.user.accessgroup.user_type)
)
Running Tests
uv pip install -e ".[dev]"
uv run pytest
Or with coverage:
uv run pytest --cov=easyjwt_auth --cov=easyjwt_client --cov=easyjwt_user
Changelog
See CHANGELOG.md for version history.
Recent Changes (1.0.0)
- Replaced MD5 with SHA-256 for password hashing
- Renamed
ModelBackendtoRemoteAuthBackend - Added configurable HTTP request timeouts
- Made SSL verification configurable
- Consolidated HTTP error handling
- Added
__all__exports to public modules
Acknowledgements
This package is heavily based on djangorestframework-simplejwt and influenced by SimpleJWT.
An example implementation is available at django-easyjwt-example.
License
MIT License - see LICENCE file.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_easyjwt-1.0.6.tar.gz.
File metadata
- Download URL: django_easyjwt-1.0.6.tar.gz
- Upload date:
- Size: 29.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5929dd99d50227f4618c1f35c580a3d84f4cf3b39262960e7b1cb32611b3252
|
|
| MD5 |
3dc55c70ca97b112ce94aadd93b73dc5
|
|
| BLAKE2b-256 |
c08ec9189cb9e48bf0f304c539b3db395b2a252946991cb35ee8d36850b57bcc
|
Provenance
The following attestation bundles were made for django_easyjwt-1.0.6.tar.gz:
Publisher:
release-publish.yml on garrethcain/django-easyjwt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_easyjwt-1.0.6.tar.gz -
Subject digest:
c5929dd99d50227f4618c1f35c580a3d84f4cf3b39262960e7b1cb32611b3252 - Sigstore transparency entry: 1842972032
- Sigstore integration time:
-
Permalink:
garrethcain/django-easyjwt@e9bb7db16e4d276ff4cb2d0da76048a3ad066d3a -
Branch / Tag:
refs/heads/master - Owner: https://github.com/garrethcain
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-publish.yml@e9bb7db16e4d276ff4cb2d0da76048a3ad066d3a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file django_easyjwt-1.0.6-py3-none-any.whl.
File metadata
- Download URL: django_easyjwt-1.0.6-py3-none-any.whl
- Upload date:
- Size: 36.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86bad4fcb6c2c0f83f04408c040666022ecf64223bfc3205e7a467a028da3cbb
|
|
| MD5 |
67a8d3079e7b1412803f82db88eb088e
|
|
| BLAKE2b-256 |
eb4050a61a6f9d4ccd6f6bfe528bc5cbbcf2925517e56fd6aa9d2d98b87c129a
|
Provenance
The following attestation bundles were made for django_easyjwt-1.0.6-py3-none-any.whl:
Publisher:
release-publish.yml on garrethcain/django-easyjwt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_easyjwt-1.0.6-py3-none-any.whl -
Subject digest:
86bad4fcb6c2c0f83f04408c040666022ecf64223bfc3205e7a467a028da3cbb - Sigstore transparency entry: 1842972130
- Sigstore integration time:
-
Permalink:
garrethcain/django-easyjwt@e9bb7db16e4d276ff4cb2d0da76048a3ad066d3a -
Branch / Tag:
refs/heads/master - Owner: https://github.com/garrethcain
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-publish.yml@e9bb7db16e4d276ff4cb2d0da76048a3ad066d3a -
Trigger Event:
workflow_dispatch
-
Statement type: