Skip to main content

A passworldess plugin for Django Rest Framework authentication package

Project description

⛔️ ALPHA -- WORK IN PROGRESS

jwt drf passwordless

A Passwordless login add-on for Django Rest Framework authentication. Built with django-sms, django-phonenumber-field and djangorestframework-simplejwt with complete statelessness in mind.

Great Thanks

This project is a fork of Sergioisidoro's djoser-passwordless project, I have mostly just modified and customized this to be more in line with my own needs and preferences. Which include statelessness, independence of other authentication packages, and a more flexible and configurable approach to the token generation and validation.

🔑 Before you start!

Please consider your risk and threat landscape before adopting this library.

Authentication is always a trade-off of usability and security. This library has been built to give you the power to adjust those trade-offs as much as possible, and made an attempt to give you a reasonable set of defaults, but it's up to you to make those decisions. Please consider the following risks bellow.

TODO

  • recaptcha verification
  • webauthn support
  • better documentation

Installation

pip install jwt_drf_passwordless

settings.py

INSTALLED_APPS = (
    ...
    "jwt_drf_passwordless",
    ...
)
...
jwt_drf_passwordless = {
    "ALLOWED_PASSWORDLESS_METHODS": ["EMAIL", "MOBILE"]
}

Remember to set the settings for django-sms and django-phonenumber-field if you are using mobile token requests

urlpatterns = (
    ...
    re_path(r"^passwordless/", include("jwt_drf_passwordless.urls")),
    ...
)

🕵️ Risks

Brute force

Although token requests are throttled by default, and token lifetime is limited, if you know a user email/phone it is possible to continuously request tokens (the default throttle is 1 minute), and try to brute force that token during the token lifetime (10 minutes).

Mitigations

  • Set INCORRECT_SHORT_TOKEN_REDEEMS_TOKEN to True, so that any attempts at redeeming a token from an account will count as a user (MAX_TOKEN_USES is default set to 1) - Tradeoff is that if a user is being a victim of brute force attack, they will not be able to login with passwordless tokens, since it's likely the attacker will exhaust the token uses with failed attempts

  • Set DECORATORS.token_redeem_rate_limit_decorator or DECORATORS.token_request_rate_limit_decorator with your choice of request throttling library. - Tradeoff is that if there is an attacker hitting your service, you might prevent any user from logging in because someone is hitting this endpoint, so beware how you implement it. Note that because request limiting usually requires a key value db like redis, it is explicitly left out of this project to reduce it's dependencies and configuration needs.

  • Use External 2FA Providers - Services like Telnyx Verify and Twilio Verify handle rate limiting, code generation, and delivery tracking. They also provide carrier-level fraud detection. - Tradeoff is vendor dependency and per-verification costs, but significantly improved security posture.

Webhook Security

When using external 2FA providers, always enable webhook signature verification to prevent spoofed delivery events. For Telnyx, configure webhook_public_key in your settings to enable Ed25519 signature verification.

Features

  • International phone number validation and standardization (expects db phone numbers to be in same format)
  • Basic throttling
  • Stateless JWT tokens by default
  • Short (for SMS) and long tokens for magic links
  • Configurable serializers, permissions and decorators
  • External 2FA provider support (Telnyx, Twilio, etc.) - delegate code generation to trusted providers

URLs and Examples:

Available URLS

Internal Token Flow (tokens generated and stored locally):

  • request/email/ - Request token via email
  • request/mobile/ - Request token via SMS
  • exchange/email/ - Exchange email token for JWT
  • exchange/mobile/ - Exchange mobile token for JWT

External 2FA Flow (tokens managed by external provider):

  • external/request/ - Request verification via external provider
  • external/verify/ - Verify code and get JWT tokens
  • external/webhook/ - Receive delivery status webhooks

Requesting a token

curl --request POST \
  --url http://localhost:8000/passwordless/request/email/ \
  --data '{
	"email": "sergioisidoro@example.com"
}'

Response

{
	"detail": "A token has been sent to you"
}

Exchanging a one time token for a auth token

curl --request POST \
  --url http://localhost:8000/passwordless/exchange/ \
  --data '{
	"email": "sergioisidoro@example.com"
	"token": "902488"
}'
{
	"refresh": "3b8e6a2aed0435f95495e728b0fb41d0367a872d",
  "access": "3b8e6a2aed0435f95495e728b0fb41d0367a872d"
}

External 2FA Provider Flow

When using an external provider like Telnyx Verify, the provider handles code generation and delivery.

Requesting verification via external provider

curl --request POST \
  --url http://localhost:8000/passwordless/external/request/ \
  --header 'Content-Type: application/json' \
  --data '{
	"phone_number": "+13035551234"
}'

Response

{
	"detail": "A token has been sent to you"
}

Verifying code and getting JWT tokens

curl --request POST \
  --url http://localhost:8000/passwordless/external/verify/ \
  --header 'Content-Type: application/json' \
  --data '{
	"phone_number": "+13035551234",
	"code": "123456"
}'
{
	"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
	"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}

Config

Basic configuration

  • ALLOWED_PASSWORDLESS_METHODS (default=["email"]) - Which methods can be used to request a token? (Valid - ["email", "mobile"])
  • EMAIL_FIELD_NAME (default="email") - Name of the user field that holds the email info
  • MOBILE_FIELD_NAME (default="phone_number") - Name of the user field that holds phone number info
  • SHORT_TOKEN_LENGTH (default=6) - The length of the short tokens
  • LONG_TOKEN_LENGTH (default=64) - The length of the tokens that can redeemed standalone (without the original request data)
  • SHORT_TOKEN_CHARS (default="0123456789") - The characters to be used when generating the short token
  • LONG_TOKEN_CHARS (default="abcdefghijklmnopqrstuvwxyz0123456789") - Tokens used to generate the long token
  • TOKEN_LIFETIME (default=600) - Number of seconds the token is valid
  • MAX_TOKEN_USES (default=1) - How many times a token can be used - This can be adjusted because some email clients try to follow links, and might accidentally use tokens.
  • TOKEN_REQUEST_THROTTLE_SECONDS - (default=60) - How many seconds to wait before allowing a new token to be issued for a particular user
  • ALLOW_ADMIN_AUTHENTICATION (default=False) - Allow admin users to login without password (checks is_admin and is_staff from Django AbstractUser)
  • REGISTER_NONEXISTENT_USERS (default=False) - Register users who do not have an account and request a passwordless login token?
  • REGISTRATION_SETS_UNUSABLE_PASSWORD (Default=True) - When unusable password is set, users cannot reset passwords via the normal Django flows. This means users registered via passwordless cannot login through password.
  • INCORRECT_SHORT_TOKEN_REDEEMS_TOKEN (default=False) - Should incorrect short token auth attempts count to the uses of a token? When set to true, together with MAX_TOKEN_USES to 1, this means a token has only one shot at being used.
  • PASSWORDLESS_EMAIL_LOGIN_URL (default=None) - URL template for the link redeeming the standalone link: eg my-app://page/{token}

Advanced configuration

External 2FA Provider

When EXTERNAL_2FA is configured, the /request/mobile/ and /exchange/mobile/ endpoints automatically delegate to the external provider instead of using internal token generation. The API contract is unchanged — same field names (phone_number, token), same response shape.

JWT_DRF_PASSWORDLESS = {
    "ALLOWED_PASSWORDLESS_METHODS": ["MOBILE"],
    "EXTERNAL_2FA": {
        "provider": "jwt_drf_passwordless.external_2fa.TelnyxVerifyProvider",
        "api_key": "YOUR_TELNYX_API_KEY",
        "verify_profile_id": "YOUR_TELNYX_VERIFY_PROFILE_ID",
        "webhook_public_key": "YOUR_TELNYX_PUBLIC_KEY",  # For webhook signature verification
    },
}

Supported Providers:

  • jwt_drf_passwordless.external_2fa.TelnyxVerifyProvider - Telnyx Verify API

Creating a Custom Provider:

Implement the External2FAProvider abstract class:

from jwt_drf_passwordless.external_2fa import External2FAProvider, External2FAResult, VerificationStatus

class MyProvider(External2FAProvider):
    def send_verification(self, phone_number, method=VerificationMethod.SMS):
        # Send code via your provider
        return External2FAResult(success=True, status=VerificationStatus.PENDING)

    def verify_code(self, phone_number, code):
        # Verify code with your provider
        return External2FAResult(success=True, status=VerificationStatus.ACCEPTED)

    def cancel_verification(self, phone_number):
        return External2FAResult(success=True, status=VerificationStatus.EXPIRED)
Webhook Configuration

External providers send delivery status updates via webhooks. Configure your provider to send webhooks to:

POST https://your-domain.com/passwordless/external/webhook/

Telnyx Webhook Setup:

  1. In Telnyx Mission Control, configure your Verify Profile webhook URL
  2. Set webhook_public_key in your config for Ed25519 signature verification
  3. Whitelist Telnyx IPs: 192.76.120.192/27

Listening to Webhook Events:

from django.dispatch import receiver
from jwt_drf_passwordless.external_2fa.signals import (
    verification_delivered,
    verification_delivery_failed,
)

@receiver(verification_delivered)
def on_delivered(sender, event, phone_number, **kwargs):
    # Log successful delivery
    pass

@receiver(verification_delivery_failed)
def on_failed(sender, event, phone_number, error, **kwargs):
    # Alert on delivery failure
    pass
Testing with FakeVerifyProvider

The package includes a built-in fake provider for use in test suites and local development. It accepts a fixed code (default "12345") with no network calls:

# settings/test.py (or conftest.py override)
JWT_DRF_PASSWORDLESS = {
    "ALLOWED_PASSWORDLESS_METHODS": ["MOBILE"],
    "EXTERNAL_2FA": {
        "provider": "jwt_drf_passwordless.external_2fa.FakeVerifyProvider",
        "api_key": "ignored",
        "verify_profile_id": "ignored",
    },
}

In your tests, POST to /passwordless/external/verify/ with "code": "12345" and it will succeed. To use a custom code:

JWT_DRF_PASSWORDLESS = {
    ...
    "EXTERNAL_2FA": {
        "provider": "jwt_drf_passwordless.external_2fa.FakeVerifyProvider",
        "code": "99999",  # passed as kwarg to the provider constructor
    },
}
Callbacks

React to verification events without subclassing views. Configure a dotted import path:

JWT_DRF_PASSWORDLESS = {
    "CALLBACKS": {
        "on_verification_accepted": "myapp.auth.on_phone_verified",
    },
}

The callback is called after a successful external 2FA verification, before JWT tokens are returned:

# myapp/auth.py
def on_phone_verified(user, phone_number, request):
    user.phone_verified = True
    user.save(update_fields=["phone_verified"])

Signature: callback(user, phone_number, request)

  • user — the authenticated Django user
  • phone_number — the verified phone number (string, E.164 format)
  • request — the DRF request object

If no callback is configured (the default), this step is skipped.

Credits

This package was created with Cookiecutter_ and the audreyr/cookiecutter-pypackage_ project template.

License

  • Free software: MIT license
  • Do no harm

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

jwt_drf_passwordless-0.3.0.tar.gz (26.1 kB view details)

Uploaded Source

Built Distribution

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

jwt_drf_passwordless-0.3.0-py3-none-any.whl (32.2 kB view details)

Uploaded Python 3

File details

Details for the file jwt_drf_passwordless-0.3.0.tar.gz.

File metadata

  • Download URL: jwt_drf_passwordless-0.3.0.tar.gz
  • Upload date:
  • Size: 26.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for jwt_drf_passwordless-0.3.0.tar.gz
Algorithm Hash digest
SHA256 119dd616f3454d09728b9dc102012d65e8b37f5036ef8fd921e3d310e4e85c22
MD5 8cf509e6ee15086466f8b4a710382f7b
BLAKE2b-256 a10dbf7c5eaaf86165ec8078dc99f5d8e52435312e599ca7b7b5660e261830ca

See more details on using hashes here.

File details

Details for the file jwt_drf_passwordless-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for jwt_drf_passwordless-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fbdea6172fd299835b5644b8ecd9f9644123057b88a1c0bde62587420134b77b
MD5 7d611dde16d343391143a17690bbea46
BLAKE2b-256 fb3587388fd0afc7bded7ba2ff7914f70025695e1ff1112ffce47a0bd0e18ff7

See more details on using hashes here.

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