Skip to main content

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

PyPI version Python Django License: MIT

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?

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 ModelBackend to RemoteAuthBackend
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_easyjwt-1.0.6.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_easyjwt-1.0.6-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

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

Hashes for django_easyjwt-1.0.6.tar.gz
Algorithm Hash digest
SHA256 c5929dd99d50227f4618c1f35c580a3d84f4cf3b39262960e7b1cb32611b3252
MD5 3dc55c70ca97b112ce94aadd93b73dc5
BLAKE2b-256 c08ec9189cb9e48bf0f304c539b3db395b2a252946991cb35ee8d36850b57bcc

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_easyjwt-1.0.6.tar.gz:

Publisher: release-publish.yml on garrethcain/django-easyjwt

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for django_easyjwt-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 86bad4fcb6c2c0f83f04408c040666022ecf64223bfc3205e7a467a028da3cbb
MD5 67a8d3079e7b1412803f82db88eb088e
BLAKE2b-256 eb4050a61a6f9d4ccd6f6bfe528bc5cbbcf2925517e56fd6aa9d2d98b87c129a

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page