Skip to main content

django-reusable-auth

tests PyPI License: MIT

Build authentication once. Configure it for every project.

django-reusable-auth is a JWT authentication API for Django REST Framework. It handles the parts most projects end up rebuilding: registration, login, email verification, password reset, sessions, and protection against brute-force attacks. It is built on Simple JWT and Django's own security primitives, with no custom cryptography.

Features:

  • Email-based user model, with case-insensitive emails
  • Registration, login, logout and token refresh
  • Email verification and password reset through single-use emailed links
  • Password change, account deactivation and session management
  • Refresh token rotation with reuse detection
  • Per-IP rate limiting and progressive account lockout
  • Responses that don't reveal which emails are registered
  • Startup checks that flag insecure configuration
  • Optional OpenAPI schema through drf-spectacular

Tested on Python 3.10–3.14 and Django 5.2–6.1, against SQLite, PostgreSQL and Redis.

Installation

pip install django-reusable-auth

Requires Django 5.2+, Django REST Framework 3.16+ and Simple JWT 5.5+, which are installed with it. For the OpenAPI schema, install django-reusable-auth[openapi] instead.

Quick start

The package provides its own user model. Django can't easily change the user model of a project that has already been migrated, so add the package before your first migrate.

Add to your settings:

from datetime import timedelta

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "rest_framework_simplejwt.token_blacklist",
    "reusable_auth",
]

AUTH_USER_MODEL = "reusable_auth.CustomUser"

# APP_DIRS lets Django find the package's email templates
TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "APP_DIRS": True,
    # ...
}]

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
}

REUSABLE_AUTH = {
    # Pages in your frontend that the emailed links open
    "EMAIL_VERIFICATION_URL": "https://app.example.com/verify-email?uid={uid}&token={token}",
    "PASSWORD_RESET_URL": "https://app.example.com/reset-password?uid={uid}&token={token}",
}

You also need an email backend (EMAIL_BACKEND, or MAILERS on Django 6.1+).

Include the URLs:

urlpatterns = [
    path("api/", include("reusable_auth.urls")),
]

Then migrate, and run Django's checks. The package reports any missing setup:

python manage.py migrate
python manage.py check

Before deploying, read Production.

The demo project is a complete working setup with Swagger UI.

Endpoints

Paths are relative to where you include the URLs (/api/ above).

Method Path Auth Request Response
POST auth/register/ first_name, last_name, email, password 201
POST auth/login/ email, password 200 access, refresh
POST auth/refresh/ refresh 200 access, refresh
POST auth/logout/ refresh 200
GET auth/me/ Bearer 200 user
POST auth/me/deactivate/ Bearer password 204
GET auth/sessions/ Bearer 200 list of sessions
DELETE auth/sessions/ Bearer 204
DELETE auth/sessions/<id>/ Bearer 204
POST auth/email/verify/ uid, token 200
POST auth/email/verify/resend/ email 200
POST auth/password/change/ Bearer old_password, new_password 200 access, refresh
POST auth/password/reset/ email 200
POST auth/password/reset/confirm/ uid, token, new_password 200

Other 200 and 201 responses contain a detail message. With drf-spectacular installed, the full schema is generated for these endpoints.

Errors

All errors use Django REST Framework's standard format:

Status Body Cause
400 {"email": ["Enter a valid email address."]} Invalid input, keyed by field
400 {"detail": "This link is invalid or has expired."} Bad or already used emailed link
401 {"detail": "Invalid email or password."} Wrong credentials, or a missing or invalid token
403 {"detail": "Verify your email address before logging in."} Correct password, but email not verified
429 {"detail": "..."} Rate limited or account locked; see the Retry-After header

The package doesn't set EXCEPTION_HANDLER, so the rest of your API is unaffected.

How it works

Registration and email verification

Registration sends a verification link, and login returns 403 (code email_not_verified) until the user opens it. Set REQUIRE_EMAIL_VERIFICATION to False to allow login without verifying.

If someone registers with an email that already has an account, the response is the same as for a new signup, and the account owner is notified by email instead. Verification resend and password reset also respond identically for every email, so none of these endpoints reveal which emails are registered.

Users created with createsuperuser are verified automatically.

Verification and reset links carry a uid and a token. Your frontend page reads them from the URL and POSTs them to the matching confirm endpoint.

Tokens are signed with SECRET_KEY and nothing is stored in the database. A link stops working once it has been used, and expires after Django's PASSWORD_RESET_TIMEOUT (3 days by default).

Sessions and logout

Each logged-in device holds one refresh token, which is listed as a session. Logout, revoking a session, changing or resetting the password, and deactivating the account all revoke refresh tokens. Access tokens already issued stay valid until they expire, so keep ACCESS_TOKEN_LIFETIME short.

Logout only needs the refresh token, since the access token has often expired by then.

Session IDs change on every refresh, because rotation issues a new token.

Refresh token reuse detection

With rotation enabled, each refresh token can be used only once. If an already-rotated token is used again, someone has a copy of it, so every session of that user is revoked and both parties have to log in again.

Two requests sending the same token within REFRESH_TOKEN_REUSE_GRACE seconds (10 by default), such as two browser tabs refreshing at once, are not treated as reuse. Tokens that were logged out or revoked are simply rejected.

Passwords and deactivation

Changing the password requires the current one. It logs out every session and returns new tokens for the current device. Resetting it also logs out every session, and marks the email as verified and clears any lockout, since the user has just proved they own the address. Both send a notification email.

Deactivation requires the password and revokes every session. Deactivating a user elsewhere, for example in the Django admin, revokes their sessions too, so reactivating the account doesn't restore them.

Rate limiting and account lockout

The public endpoints, password change and deactivation are rate-limited per IP address. Separately, an account is locked after 5 failed logins within 15 minutes, however many IPs they come from. The lock lasts 15 minutes and doubles each time it recurs, up to 24 hours. Both return 429 with a Retry-After header.

Anyone who knows an email can trigger its lockout. Locks expire on their own, and an administrator can clear one early:

python manage.py unlock_account jane@example.com

Configuration

Package settings

Set any of these in a REUSABLE_AUTH dict in your settings:

Setting Default Description
REQUIRE_EMAIL_VERIFICATION True Block login until the email is verified
EMAIL_VERIFICATION_URL None Frontend URL for verification links, with {uid} and {token}
PASSWORD_RESET_URL None Frontend URL for reset links, with {uid} and {token}
EMAIL_SENDER built-in Dotted path to a function that sends the emails
EMAIL_COOLDOWN 60 Seconds before the same email can be sent to the same address again
LOGIN_MAX_FAILURES 5 Failed logins before an account is locked
LOGIN_FAILURE_WINDOW 900 Seconds failed logins are counted over
LOGIN_LOCKOUT_DURATION 900 Seconds the first lock lasts
LOGIN_LOCKOUT_MAX_DURATION 86400 Longest lock, after repeated doubling
LOGIN_LOCKOUT_RESET_AFTER 86400 Seconds without a lock before durations return to the first one
REFRESH_TOKEN_REUSE_GRACE 10 Seconds a just-rotated token can be resent without counting as reuse

Rate limits

Override them in REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"], or set a scope to None to disable it:

Scope Default Endpoints
auth_login 10/min Login
auth_register 10/hour Register
auth_refresh 30/min Refresh
auth_logout 30/min Logout
auth_password_reset 5/hour Request a password reset
auth_verification_resend 5/hour Resend verification
auth_link_confirm 30/hour Verify email, confirm password reset
auth_account_change 10/hour Change password, deactivate

Emails

The package sends four emails: verify_email, password_reset, password_changed and account_exists. Each is rendered from reusable_auth/email/<name>_subject.txt and <name>_body.txt, plus <name>_body.html if it exists. To change the wording, add a template at the same path in your project.

To send emails another way, such as through a task queue, set EMAIL_SENDER to a function accepting kind, user and context keyword arguments.

Sending failures are logged to the reusable_auth.emails logger rather than returned as errors, so that a failed send doesn't reveal that an account exists.

Production

  • Use a shared cache, such as Redis. Rate limits, lockouts and reuse detection are stored in Django's cache. The default LocMemCache is separate for each process and cleared on restart, which weakens all three.

    CACHES = {
        "default": {
            "BACKEND": "django.core.cache.backends.redis.RedisCache",
            "LOCATION": "redis://127.0.0.1:6379",
        }
    }
    
  • Set NUM_PROXIES in REST_FRAMEWORK if the app runs behind a proxy or load balancer. Otherwise every client appears to have the proxy's IP and shares one rate limit. Setting it too high lets clients spoof their IP.

  • Keep SECRET_KEY secret. It signs every token and emailed link; anyone who has it can log in as any user.

  • Send emails in the background, through EMAIL_SENDER. Sending during the request makes password reset measurably slower for registered emails than for unknown ones.

  • Keep ACCESS_TOKEN_LIFETIME short, since access tokens can't be revoked.

System checks

The package registers checks that run with migrate, runserver and manage.py check:

ID Problem
reusable_auth.E001 The cache is DummyCache, so rate limiting is disabled
reusable_auth.E002 rest_framework_simplejwt.token_blacklist isn't installed
reusable_auth.E003 The email templates can't be loaded
reusable_auth.W001 The cache is LocMemCache while DEBUG is off
reusable_auth.W002 REUSABLE_AUTH contains an unknown setting
reusable_auth.W003 Refresh token rotation is off, so reuse detection is disabled
reusable_auth.W004 EMAIL_VERIFICATION_URL or PASSWORD_RESET_URL isn't set

Lockouts are logged to the reusable_auth.security logger, with emails hashed.

Roadmap

Social login, two-factor authentication and passkeys are not part of 1.x but may be added later.

Contributing

Contributions are welcome. See CONTRIBUTING.md for the development setup, and the changelog for release notes.

Please report security issues privately, as described in SECURITY.md, rather than in a public issue.

License

MIT

Metadata

Release files for django-reusable-auth 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-reusable-auth 1.0.1
File Size Uploaded
django_reusable_auth-1.0.1.tar.gz 36.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-reusable-auth 1.0.1
File Interpreter ABI Platform
django_reusable_auth-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 71.3 kB

Release files / django_reusable_auth-1.0.1.tar.gz

Download URL django_reusable_auth-1.0.1.tar.gz
Size 36.7 kB
Tags Source
SHA-256 checksum
How to use checksums
01196c817c125784f82ab68d84279c5d7b32fa3db5f6c3b4d47d632b08f32bf6
BLAKE2b-256 checksum
How to use checksums
fe47e5fe9a674eda3e623372d41c81b4e7fbcb3d300f6d7a3031879ce7296111
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / django_reusable_auth-1.0.1-py3-none-any.whl

Download URL django_reusable_auth-1.0.1-py3-none-any.whl
Size 34.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7dc99a7a9ce48c7f64a8d530f8276b48442092927ab4947fa6ae1be66f9e0eb6
BLAKE2b-256 checksum
How to use checksums
fbd15f101f73dde7cc4d9b91f2ebb83d6380dbf16b8968c28a82cf194c8cd787
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page