Skip to main content

forge-auth

Application Django réutilisable fournissant un système d'authentification complet : utilisateur personnalisé, connexion par mot de passe ou par code OTP (one-time password), JWT (header ou cookie httponly), gestion de groupes/permissions et endpoints REST prêts à l'emploi (Django REST Framework).

Sommaire

  • Fonctionnalités
  • Installation
  • Configuration rapide
  • Assistant de configuration interactif (forge_auth_setup)
  • Référence complète des options FORGE_AUTH
  • Scénarios de configuration détaillés
  • Endpoints de l'API
  • Permissions : IsSelfOrAdmin
  • Throttling
  • Vérification de contact (email/téléphone)
  • Verrouillage de compte
  • Sessions et historique de connexion
  • MFA TOTP applicatif
  • Connexion sans mot de passe (magic link)
  • Clés API (M2M)
  • Connexion sociale (OIDC)
  • Exemples d'utilisation
  • Modèle User : méthodes et propriétés utiles
  • Signal user_logged_in
  • Signal otp_requested
  • Signal password_reset_requested
  • Signal contact_verification_requested
  • Signal magic_link_requested
  • Avertissement sur les migrations
  • Points non automatisés (à implémenter côté projet hôte)
  • Notes de sécurité
  • Lancer les tests

Fonctionnalités

  • Modèle User personnalisé sans champ username imposé : authentification par phone_number, email, ou les deux.
  • Champs status (vérification de compte), otp_secret (TOTP) et profile_photo (photo de profil) optionnels et désactivables.
  • Authentification par mot de passe, par code OTP, sans mot de passe (magic link) ou via un fournisseur OIDC (Google, Microsoft...).
  • Second facteur (MFA) TOTP applicatif optionnel (Google Authenticator...), avec codes de secours.
  • JWT via header Authorization: Bearer ou via cookies httponly, au choix (les deux peuvent être actifs simultanément), avec rotation optionnelle des refresh tokens.
  • Backend d'authentification Django supportant plusieurs champs de connexion (MultiFieldBackend).
  • Clés API pour l'authentification machine-à-machine (ApiKeyAuthentication, optionnelle).
  • ViewSets DRF prêts à l'emploi : inscription, connexion, déconnexion, rafraîchissement de token, vérification d'unicité email/téléphone, utilisateur courant, vérification de session, changement de mot de passe, mot de passe oublié, vérification de contact, gestion des sessions/appareils, historique de connexion.
  • Contrôle d'accès par objet (IsSelfOrAdmin) sur /users/ : un utilisateur ne voit/modifie que lui-même, le staff voit tout.
  • Verrouillage de compte configurable après un nombre d'échecs de connexion.
  • Limitation de débit (throttling) configurable sur les endpoints sensibles (login, OTP, refresh, reset de mot de passe, vérification d'existence).
  • Documentation OpenAPI via drf-spectacular (extend_schema déjà posé sur chaque action).
  • Validation de configuration au démarrage (AppConfig.ready()), qui stoppe le serveur si FORGE_AUTH est mal formé.
  • Assistant interactif (python manage.py forge_auth_setup) pour générer FORGE_AUTH sans avoir à connaître toutes les options à l'avance.

Installation

Le package est structuré en layout src/ et se construit avec hatchling. Avec uv, depuis le projet Django qui consomme forge-auth :

# Installation depuis un chemin local
uv add /chemin/vers/forge_auth

# Ou depuis un dépôt git
uv add git+https://exemple.com/forge_auth.git

# Ou en mode editable pendant le développement du package lui-même
uv pip install -e /chemin/vers/forge_auth

Dépendances installées automatiquement : django, djangorestframework, djangorestframework-simplejwt, pyotp, drf-spectacular, pillow (photo de profil), pyjwt[crypto] (vérification des id_token OIDC pour la connexion sociale).

Configuration rapide

Dans settings.py du projet hôte :

INSTALLED_APPS = [
    # ...
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "rest_framework",
    "rest_framework_simplejwt.token_blacklist",
    "forge_auth",
]

AUTH_USER_MODEL = "forge_auth.User"

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "forge_auth.authentification.JWTAuthenticationFlexible",
    ],
}

# Nécessaire uniquement si vous voulez l'authentification Django classique
# (admin, formulaires) avec plusieurs champs de login.
AUTHENTICATION_BACKENDS = [
    "forge_auth.backends.MultiFieldBackend",
    "django.contrib.auth.backends.ModelBackend",
]

# [] par défaut dans Django (pas de validation) : à renseigner explicitement
# si vous voulez que la création de compte, le changement de mot de passe et
# la réinitialisation de mot de passe rejettent les mots de passe faibles.
AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

# Optionnel : débits de limitation de requêtes sur les endpoints sensibles
# de forge_auth (no-op si absent, voir section "Throttling").
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
    "forge_auth_login": "10/min",
    "forge_auth_otp": "5/min",
    "forge_auth_refresh": "30/min",
    "forge_auth_password_reset": "5/min",
    "forge_auth_verify": "20/min",
}

# Optionnel : authentification par clé API (M2M) en plus du JWT — voir
# section "Clés API (M2M)".
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
    "forge_auth.authentification.ApiKeyAuthentication"
)

# Nécessaire uniquement si OPTIONAL_FIELDS ne désactive pas "profile_photo"
# (activé par défaut) : emplacement de stockage des photos de profil.
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

FORGE_AUTH = {}  # voir section "Référence complète" et "Scénarios"

Dans urls.py du projet hôte :

from django.urls import include, path

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

Les routes de forge_auth.urls incluent déjà le préfixe forge_auth/ : avec l'exemple ci-dessus, l'endpoint de connexion devient /api/forge_auth/users/login/.

Servir les fichiers médias en développement (photo de profil) :

from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Puis :

python manage.py migrate

Assistant de configuration interactif (forge_auth_setup)

Pour configurer FORGE_AUTH sans avoir à connaître toutes les options à l'avance (voir la référence complète ci-dessous), une commande de gestion interactive est fournie — disponible dès que forge_auth est dans INSTALLED_APPS :

uv run manage.py forge_auth_setup
# ou, sans uv :
python manage.py forge_auth_setup

Déroulement :

  1. Choix de l'identifiant de connexion (USERNAME_FIELD/ALTERNATIVE_USERNAME_FIELDS) et des champs du modèle User à activer/désactiver (OPTIONAL_FIELDS) — présentés comme une liste à cocher (case [x]/[ ]) : tapez les numéros à basculer, Entrée pour valider la sélection affichée.
  2. Choix des fonctionnalités à configurer maintenant (OTP, JWT, verrouillage de compte, MFA TOTP, magic link, connexion sociale, groupes/superutilisateur) — même principe de liste à cocher. Une fonctionnalité non cochée garde sa valeur par défaut (voir la référence des options) : pas besoin de répondre à des questions sur des fonctionnalités que vous n'utilisez pas.
  3. Pour chaque fonctionnalité cochée, une série de questions ciblées avec valeur par défaut proposée entre crochets (Entrée pour l'accepter).
  4. Aperçu complet du bloc FORGE_AUTH généré, puis confirmation explicite avant toute écriture — rien n'est modifié si vous répondez non.
  5. Le bloc validé est ajouté à la fin du fichier de settings du projet hôte (déduit de DJANGO_SETTINGS_MODULE, ou précisé via --settings-file).
$ uv run manage.py forge_auth_setup
Assistant de configuration forge_auth
...
2. Champs du modèle User
Cochez les champs à ACTIVER (numéros séparés par des virgules pour basculer, Entrée pour valider) :
  [x] 1. Statut de vérification de compte (status)
  [x] 2. Secret OTP applicatif (otp_secret)
  [x] 3. Photo de profil (profile_photo)
Numéros à basculer, ou Entrée pour valider : 2
  [x] 1. Statut de vérification de compte (status)
  [ ] 2. Secret OTP applicatif (otp_secret)
  [x] 3. Photo de profil (profile_photo)
Numéros à basculer, ou Entrée pour valider :
...
Aperçu du bloc à ajouter
FORGE_AUTH = {   'USERNAME_FIELD': 'phone_number',
    ...
Ajouter ce bloc à la fin de /chemin/vers/settings.py ? [o/N] :

Points d'attention :

  • Si FORGE_AUTH est déjà défini dans le fichier cible, la commande le détecte et prévient qu'ajouter un nouveau bloc à la fin écrasera silencieusement l'ancien à l'exécution (Python exécute le fichier de haut en bas) — elle demande une confirmation explicite avant de continuer, et s'arrête par défaut.
  • La commande n'écrit jamais sans l'aperçu + la confirmation finale ; Ctrl-C à tout moment annule proprement sans rien modifier.
  • Le mot de passe du superutilisateur par défaut est saisi via une entrée masquée (getpass), mais apparaît en clair dans l'aperçu final et dans le fichier écrit — c'est CREDENTIALS_SUPERUSER, un mot de passe de bootstrap à changer avant la mise en production, pas un secret géré différemment du reste de settings.py.
  • Elle ne touche jamais INSTALLED_APPS/AUTH_USER_MODEL/REST_FRAMEWORK (trop risqué de les modifier par simple ajout de texte sans analyser le fichier) : elle rappelle ces étapes restantes en fin d'exécution.

Référence complète des options FORGE_AUTH

Toutes les clés sont optionnelles ; les valeurs ci-dessous sont les valeurs par défaut.

Clé Type Défaut Rôle
USERNAME_FIELD "phone_number" | "email" "phone_number" Champ utilisé comme identifiant principal de connexion.
ALTERNATIVE_USERNAME_FIELDS list[str] [] Champs additionnels acceptés comme identifiant (ex. ["email"]).
OPTIONAL_FIELDS list["status" | "otp_secret" | "profile_photo"] [] Champs à retirer du modèle User. Présents dans cette liste = désactivés.
OTP dict voir ci-dessous Configuration du système OTP.
OTP.USE_OTP bool True Active la connexion par code OTP plutôt que par mot de passe.
OTP.OTP_LIFETIME int (secondes) 300 Durée de vie indicative du code (non appliquée automatiquement, voir plus bas).
OTP.OTP_DIGITS int 4 Nombre de chiffres du code généré.
OTP.OTP_CANAL "SMS" | "APP" | "MAIL" | "WHATSAPP" "WHATSAPP" Canal prévu pour la distribution du code (métadonnée, voir "Points non automatisés").
JWT dict voir ci-dessous Configuration de la distribution des tokens.
JWT.VIA_JSON bool True Renvoie access/refresh dans le corps JSON de la réponse de login.
JWT.VIA_HTTP_ONLY bool False Pose access/refresh en cookies httponly.
JWT.ROTATE_REFRESH_TOKENS bool False Si True, refresh blackliste l'ancien refresh token et en renvoie un nouveau (nécessite token_blacklist).
REGISTER_INCLUDE_IN_OTP bool False Si True, obtain-otp crée l'utilisateur s'il n'existe pas encore (auto-inscription via OTP).
CREDENTIALS_SUPERUSER dict {username, password} {"username": "admin", "password": "admin"} Superutilisateur créé automatiquement au premier migrate si aucun n'existe.
GROUP_DEFAULT str | None None Groupe assigné automatiquement à tout nouvel utilisateur créé sans groups explicite.
GROUPS list[str] [] Groupes créés automatiquement au migrate.
ACCOUNT_LOCKOUT dict voir ci-dessous Verrouillage de compte après des échecs de connexion répétés.
ACCOUNT_LOCKOUT.MAX_ATTEMPTS int | None 5 Nombre d'échecs consécutifs avant verrouillage. None/0 désactive la fonctionnalité.
ACCOUNT_LOCKOUT.LOCKOUT_DURATION int (secondes) 900 Durée du verrouillage.
MFA_TOTP dict voir ci-dessous Second facteur TOTP applicatif.
MFA_TOTP.ISSUER_NAME str "ForgeAuth" Nom affiché dans l'application d'authentification (Google Authenticator...).
MFA_TOTP.BACKUP_CODES_COUNT int 10 Nombre de codes de secours générés à l'activation.
MAGIC_LINK dict voir ci-dessous Connexion sans mot de passe.
MAGIC_LINK.ENABLED bool False Active request-magic-link/confirm-magic-link (405 sinon).
MAGIC_LINK.LIFETIME int (secondes) 900 Durée de validité du lien.
SOCIAL_AUTH dict[str, dict] {} Fournisseurs OIDC configurés, ex. {"google": {"ISSUER": "...", "CLIENT_ID": "..."}}.

Toute clé inconnue ou mal typée fait échouer le démarrage de Django avec un message listant précisément les erreurs (ImproperlyConfigured).

Scénarios de configuration détaillés

Scénario 1 — Défaut : téléphone + OTP WhatsApp

Aucune configuration nécessaire :

FORGE_AUTH = {}

Flux de connexion :

  1. POST /forge_auth/users/ pour créer le compte (phone_number requis).
  2. POST /forge_auth/users/obtain-otp/ avec {"username": "<phone_number>"} génère et stocke un code.
  3. POST /forge_auth/users/login/ avec {"username": "<phone_number>", "code": "<code>"}.

Scénario 2 — Email + mot de passe classique, sans OTP ni statut

FORGE_AUTH = {
    "USERNAME_FIELD": "email",
    "OPTIONAL_FIELDS": ["status", "otp_secret"],
    "OTP": {"USE_OTP": False},
}

OPTIONAL_FIELDS retire StatusMixin et OtpSecretMixin du modèle User ; OtpToken redevient une classe factice. Flux de connexion :

POST /forge_auth/users/login/
{"username": "alice@exemple.com", "password": "motdepasse"}

Voir "Avertissement sur les migrations" avant d'utiliser ce scénario en production.

Scénario 3 — Identifiant multiple (email ou téléphone) + mot de passe

FORGE_AUTH = {
    "USERNAME_FIELD": "email",
    "ALTERNATIVE_USERNAME_FIELDS": ["phone_number"],
    "OPTIONAL_FIELDS": ["otp_secret"],
    "OTP": {"USE_OTP": False},
}

L'utilisateur peut se connecter en envoyant indifféremment son email ou son numéro dans le champ username. Pensez à garder MultiFieldBackend dans AUTHENTICATION_BACKENDS si vous utilisez aussi l'authentification Django standard (admin, par exemple).

Scénario 4 — JWT uniquement en cookies httponly (pas de token dans le corps JSON)

FORGE_AUTH = {
    "JWT": {"VIA_JSON": False, "VIA_HTTP_ONLY": True},
}

La réponse de login ne contient alors pas de corps JSON exploitable côté client JavaScript ; les cookies access et refresh sont posés directement par le serveur. Adapté à un frontend servi par le même domaine, qui n'a pas besoin de manipuler les tokens lui-même. Le cookie est marqué secure automatiquement dès que DEBUG = False.

Scénario 5 — OTP par SMS, statut désactivé, OTP conservé

FORGE_AUTH = {
    "OPTIONAL_FIELDS": ["status"],
    "OTP": {"OTP_CANAL": "SMS", "OTP_DIGITS": 6},
}

Le champ status (vérification/blocage de compte) disparaît du modèle, mais l'OTP reste actif avec un code à 6 chiffres. OTP_CANAL est une métadonnée que votre code applicatif peut lire (forge_auth_config.otp_conf.OTP_CANAL) pour choisir le bon prestataire d'envoi — voir "Points non automatisés".

Scénario 6 — Auto-inscription par OTP (pas de formulaire d'inscription)

FORGE_AUTH = {
    "REGISTER_INCLUDE_IN_OTP": True,
}

POST /forge_auth/users/obtain-otp/ avec un numéro inconnu crée silencieusement l'utilisateur avant de générer le code, au lieu de renvoyer une erreur de validation. Utile pour un flux "connexion = inscription" piloté uniquement par numéro de téléphone.

Endpoints de l'API

Chemins relatifs au préfixe forge_auth/ exposé par forge_auth.urls.

Méthode Chemin Action Authentification requise
GET groups/ Liste des groupes Non
GET groups/{id}/ Détail d'un groupe Non
POST users/ Inscription Non
GET users/ Liste des utilisateurs Oui, staff uniquement
GET users/{id}/ Détail d'un utilisateur Oui, soi-même ou staff
PATCH / PUT users/{id}/ Modification d'un utilisateur Oui, soi-même ou staff
DELETE users/{id}/ Suppression d'un utilisateur Oui, soi-même ou staff
POST users/verify-email/ Vérifie si un email existe déjà Non
POST users/verify-phone/ Vérifie si un téléphone existe déjà Non
GET users/current/ Utilisateur courant Oui
POST users/login/ Connexion (mot de passe ou OTP selon config) Non
POST users/logout/ Déconnexion (blackliste le refresh token) Oui
GET users/session-check/ Vérifie que la session/JWT est valide Oui
POST users/refresh/ Rafraîchit le token d'accès Non (validé par le refresh token lui-même)
POST users/obtain-otp/ Génère et stocke un code OTP Non
POST users/authenticate-user/ Étape 1 du flux F2FA (vérifie le mot de passe) Non
POST users/verify-otp-and-login/ Étape 2 du flux F2FA (vérifie l'OTP et connecte) Non
POST users/change-password/ Change son propre mot de passe (ancien mot de passe requis) Oui
POST users/request-password-reset/ Démarre le flux mot de passe oublié (génère un token) Non
POST users/confirm-password-reset/ Termine le flux mot de passe oublié (applique le nouveau mot de passe) Non
POST users/request-contact-verification/ Demande la vérification d'un champ de contact (email/téléphone) Oui
POST users/confirm-contact-verification/ Confirme la vérification (bascule status à verified) Oui
POST users/mfa-totp-setup/ Démarre la configuration d'un second facteur TOTP Oui
POST users/mfa-totp-confirm/ Active le second facteur (renvoie les codes de secours) Oui
POST users/mfa-totp-disable/ Désactive le second facteur (mot de passe requis) Oui
POST users/request-magic-link/ Demande un lien de connexion sans mot de passe Non
POST users/confirm-magic-link/ Confirme le lien et délivre un JWT Non
GET users/sessions/ Liste les sessions actives (appareils) Oui
POST users/revoke-session/ Révoque une session précise Oui
GET users/login-history/ Historique de connexion (50 dernières tentatives) Oui
GET users/api-keys/ Liste mes clés API Oui
POST users/create-api-key/ Crée une clé API (clé en clair renvoyée une seule fois) Oui
POST users/revoke-api-key/ Révoque une clé API Oui
POST users/social-login/ Connexion via un fournisseur OIDC configuré Non

Changement de mot de passe

curl -X POST http://localhost:8000/api/forge_auth/users/change-password/ \
  -H "Authorization: Bearer <access>" \
  -H "Content-Type: application/json" \
  -d '{"old_password": "ancien", "new_password": "nouveauMotDePasseSolide"}'

Le nouveau mot de passe est validé par AUTH_PASSWORD_VALIDATORS (settings Django standard). Les refresh tokens en cours sont blacklistés (si rest_framework_simplejwt.token_blacklist est installé) : les autres sessions ouvertes avec l'ancien mot de passe sont invalidées.

Mot de passe oublié

Flux en deux étapes, basé sur django.contrib.auth.tokens.default_token_generator (stateless — aucun champ ni migration supplémentaire) :

# 1. Demande de réinitialisation : génère un token et envoie le signal
#    password_reset_requested (voir plus bas — l'envoi réel du token par
#    email/SMS est à la charge du projet hôte).
curl -X POST http://localhost:8000/api/forge_auth/users/request-password-reset/ \
  -H "Content-Type: application/json" \
  -d '{"username": "+225000000001"}'

# 2. Confirmation avec le token reçu (par email/SMS via le signal ci-dessus)
curl -X POST http://localhost:8000/api/forge_auth/users/confirm-password-reset/ \
  -H "Content-Type: application/json" \
  -d '{"username": "+225000000001", "token": "<token>", "new_password": "nouveauMotDePasseSolide"}'

Le token expire après PASSWORD_RESET_TIMEOUT (setting Django, 3 jours par défaut) et devient automatiquement invalide dès que le mot de passe change (il est dérivé du hash du mot de passe). Comme pour change-password, les refresh tokens en cours sont blacklistés après une réinitialisation réussie.

Permissions : IsSelfOrAdmin

forge_auth.permissions.IsSelfOrAdmin (câblée dans UserViewSet.get_permissions()) empêche l'IDOR sur /users/ :

  • list : réservé aux utilisateurs avec is_staff=True.
  • retrieve / update / partial_update / destroy : autorisés uniquement à l'utilisateur concerné (obj.pk == request.user.pk) ou à un membre du staff.

Sans cette permission, IsAuthenticated seul permettrait à n'importe quel utilisateur connecté de lire, modifier ou supprimer n'importe quel autre compte en changeant simplement le {id} dans l'URL.

Throttling

Les actions sensibles (login, authenticate-user, obtain-otp, verify-otp-and-login, refresh, request-password-reset, confirm-password-reset, verify-email, verify-phone, request-magic-link, confirm-magic-link, social-login) utilisent forge_auth.throttling.ForgeAuthScopedRateThrottle, une variante de ScopedRateThrottle de DRF qui ne fait rien par défaut : elle ne limite le débit d'une action que si son scope est explicitement configuré dans REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] du projet hôte (sinon ImproperlyConfigured planterait la requête, ce que ForgeAuthScopedRateThrottle évite).

Scopes utilisés :

Scope Actions concernées
forge_auth_login login, authenticate-user, request-magic-link, confirm-magic-link, social-login
forge_auth_otp obtain-otp, verify-otp-and-login
forge_auth_refresh refresh
forge_auth_password_reset request-password-reset, confirm-password-reset
forge_auth_verify verify-email, verify-phone (anti-énumération de comptes)

Exemple de configuration (voir aussi "Configuration rapide") :

REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
    "forge_auth_login": "10/min",
    "forge_auth_otp": "5/min",
    "forge_auth_refresh": "30/min",
    "forge_auth_password_reset": "5/min",
    "forge_auth_verify": "20/min",
}

Vérification de contact (email/téléphone)

Flux en deux étapes, sur le même principe que "Mot de passe oublié" (token stateless via django.core.signing, aucune migration dédiée). Le champ vérifié ("email" ou "phone_number") doit être renseigné sur le compte de l'utilisateur authentifié.

# 1. Demande de vérification : envoie le signal contact_verification_requested
curl -X POST http://localhost:8000/api/forge_auth/users/request-contact-verification/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" \
  -d '{"field": "email"}'

# 2. Confirmation avec le token reçu (par email/SMS via le signal ci-dessus)
curl -X POST http://localhost:8000/api/forge_auth/users/confirm-contact-verification/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" \
  -d '{"field": "email", "token": "<token>"}'

La confirmation bascule status à verified (si le champ status est activé). Le token encode la valeur du champ au moment de la demande : s'il change avant la confirmation (email modifié entre-temps), l'ancien token devient invalide. Un seul statut verified global existe (pas un flag par champ) — confirmer l'email ou le téléphone a le même effet sur status.

Verrouillage de compte

FORGE_AUTH["ACCOUNT_LOCKOUT"] protège login, authenticate-user et verify-otp-and-login contre le brute force applicatif (en complément du throttling, qui protège par IP/scope) : après MAX_ATTEMPTS échecs de mot de passe/OTP/TOTP consécutifs, le compte est verrouillé pendant LOCKOUT_DURATION secondes, même si les bons identifiants sont ensuite fournis. Une connexion réussie réinitialise le compteur. Désactivable avec MAX_ATTEMPTS: None.

FORGE_AUTH = {
    "ACCOUNT_LOCKOUT": {"MAX_ATTEMPTS": 5, "LOCKOUT_DURATION": 900},
}

Sessions et historique de connexion

Chaque connexion réussie (login, verify-otp-and-login, confirm-magic-link, social-login) enregistre une SessionMetadata (device/IP/dernière activité) liée au refresh token émis. logout et revoke-session la marquent révoquée et blacklistent le refresh token correspondant (nécessite rest_framework_simplejwt.token_blacklist).

curl http://localhost:8000/api/forge_auth/users/sessions/ -H "Authorization: Bearer <access>"
# [{"pk": 1, "user_agent": "...", "ip_address": "...", "created_at": "...", "last_seen_at": "..."}]

curl -X POST http://localhost:8000/api/forge_auth/users/revoke-session/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" -d '{"session_id": 1}'

Toute tentative de connexion (réussie ou non) est aussi tracée dans LoginAuditLog, consultable via users/login-history/ (50 dernières entrées de l'utilisateur authentifié — les échecs sur un identifiant inconnu sont tracés sans user rattaché, utile pour repérer une campagne de brute force côté admin).

MFA TOTP applicatif

Second facteur applicatif (Google Authenticator, Authy...), indépendant de l'OTP SMS/WhatsApp qui sert de méthode de connexion principale (FORGE_AUTH["OTP"]) : celui-ci est un facteur additionnel, activé volontairement par l'utilisateur, vérifié en plus du mot de passe/OTP lors du login.

# 1. Démarre la configuration : à encoder en QR code côté client
curl -X POST http://localhost:8000/api/forge_auth/users/mfa-totp-setup/ -H "Authorization: Bearer <access>"
# {"secret": "...", "provisioning_uri": "otpauth://totp/..."}

# 2. Confirme avec un code généré par l'app d'authentification, renvoie les codes de secours (à afficher une seule fois)
curl -X POST http://localhost:8000/api/forge_auth/users/mfa-totp-confirm/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" -d '{"code": "123456"}'
# {"backup_codes": ["a1b2c3d4", ...]}

# 3. Login désormais requis avec `totp_code` (ou `backup_code`, usage unique) en plus du mot de passe/OTP
curl -X POST http://localhost:8000/api/forge_auth/users/login/ \
  -H "Content-Type: application/json" \
  -d '{"username": "+225000000001", "password": "motdepasse", "totp_code": "123456"}'

# Désactivation (mot de passe requis)
curl -X POST http://localhost:8000/api/forge_auth/users/mfa-totp-disable/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" -d '{"password": "motdepasse"}'

Connexion sans mot de passe (magic link)

Désactivé par défaut (FORGE_AUTH["MAGIC_LINK"]["ENABLED"] = False, 405 sinon). Token stateless (django.core.signing, durée de vie MAGIC_LINK.LIFETIME).

FORGE_AUTH = {"MAGIC_LINK": {"ENABLED": True, "LIFETIME": 900}}
curl -X POST http://localhost:8000/api/forge_auth/users/request-magic-link/ \
  -H "Content-Type: application/json" -d '{"username": "+225000000001"}'
# -> signal magic_link_requested (envoi du lien à la charge du projet hôte)

curl -X POST http://localhost:8000/api/forge_auth/users/confirm-magic-link/ \
  -H "Content-Type: application/json" -d '{"token": "<token>"}'
# -> délivre un JWT, comme un login classique

Clés API (M2M)

forge_auth.authentification.ApiKeyAuthentication (header Authorization: Api-Key <clé>) n'est pas activée par défaut : à ajouter explicitement à REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] (voir "Configuration rapide") pour accepter des clés API en plus du JWT. La clé en clair n'est jamais stockée (seul son hash, via make_password) et n'est renvoyée qu'une seule fois, à sa création.

curl -X POST http://localhost:8000/api/forge_auth/users/create-api-key/ \
  -H "Authorization: Bearer <access>" -H "Content-Type: application/json" -d '{"name": "CI"}'
# {"pk": 1, "name": "CI", "prefix": "...", "key": "<clé en clair, à noter maintenant>"}

curl http://localhost:8000/api/forge_auth/users/current/ -H "Authorization: Api-Key <clé>"

Connexion sociale (OIDC)

Flux OIDC générique (pas de SDK spécifique à un fournisseur) : le client obtient un id_token via le SDK du fournisseur (web/mobile), forge_auth le vérifie via les clés publiques JWKS de l'émetteur (forge_auth.social.verify_id_token, basé sur PyJWT/PyJWKClient). N'importe quel fournisseur conforme OpenID Connect fonctionne (Google, Microsoft...).

FORGE_AUTH = {
    "SOCIAL_AUTH": {
        "google": {"ISSUER": "https://accounts.google.com", "CLIENT_ID": "<client_id>.apps.googleusercontent.com"},
    },
}
curl -X POST http://localhost:8000/api/forge_auth/users/social-login/ \
  -H "Content-Type: application/json" \
  -d '{"provider": "google", "id_token": "<id_token obtenu côté client>"}'

Le compte est lié par (provider, sub) (SocialAccount), pas par email (une adresse peut changer ou ne pas être vérifiée par le fournisseur). La création automatique d'un compte au premier login social nécessite USERNAME_FIELD = "email" (le fournisseur ne communique pas de numéro de téléphone) ; sinon la requête échoue en 400 plutôt que de créer un compte incomplet.

Exemples d'utilisation

Inscription (scénario par défaut, téléphone) :

curl -X POST http://localhost:8000/api/forge_auth/users/ \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+225000000001", "email": "alice@exemple.com"}'

Demande de code OTP :

curl -X POST http://localhost:8000/api/forge_auth/users/obtain-otp/ \
  -H "Content-Type: application/json" \
  -d '{"username": "+225000000001"}'

Connexion avec code OTP :

curl -X POST http://localhost:8000/api/forge_auth/users/login/ \
  -H "Content-Type: application/json" \
  -d '{"username": "+225000000001", "code": "1234"}'

Réponse (mode JWT.VIA_JSON = True) :

{
  "access": "<jwt>",
  "refresh": "<jwt>",
  "user": {"pk": 1, "phone_number": "+225000000001", "email": "alice@exemple.com", "...": "..."}
}

Connexion avec mot de passe (OTP désactivé) :

curl -X POST http://localhost:8000/api/forge_auth/users/login/ \
  -H "Content-Type: application/json" \
  -d '{"username": "alice@exemple.com", "password": "motdepasse"}'

Appel authentifié (header) :

curl http://localhost:8000/api/forge_auth/users/current/ \
  -H "Authorization: Bearer <access>"

Rafraîchissement du token :

curl -X POST http://localhost:8000/api/forge_auth/users/refresh/ \
  -H "Authorization: Bearer <access>" \
  -H "Content-Type: application/json" \
  -d '{"refresh": "<refresh>"}'

Déconnexion :

curl -X POST http://localhost:8000/api/forge_auth/users/logout/ \
  -H "Authorization: Bearer <access>" \
  -H "Content-Type: application/json" \
  -d '{"refresh": "<refresh>"}'

Vérification d'unicité avant inscription (front-end) :

curl -X POST http://localhost:8000/api/forge_auth/users/verify-email/ \
  -H "Content-Type: application/json" \
  -d '{"verify": "alice@exemple.com"}'

Modèle User : méthodes et propriétés utiles

  • user.username : retourne la valeur du champ configuré comme USERNAME_FIELD.
  • user.full_name : "Prénom Nom".
  • user.is_valid_email / user.is_valid_phone_number : validité syntaxique.
  • User.get(username) : recherche sur USERNAME_FIELD et ALTERNATIVE_USERNAME_FIELDS, lève User.DoesNotExist ou PermissionError (compte au statut deleted, uniquement si status est activé).
  • Si status est activé : user.is_verified, user.is_unauthorized, et les méthodes mark_as_verified(), mark_as_unverified(), mark_as_suspended(), deactivate_user(), delete_user(). is_active=False et is_unauthorized (statuts blocked/suspended/deleted/deactivated) sont tous les deux vérifiés par login, authenticate-user et verify-otp-and-login : un compte désactivé/bloqué/suspendu ne peut plus obtenir de nouveau JWT (401), même avec le bon mot de passe/code.
  • Si otp_secret est activé et OTP.USE_OTP est True : user.otp_token.generate_otp() / user.otp_token.verify_otp(code).

Signal user_logged_in

forge_auth.signals.user_logged_in est un django.dispatch.Signal envoyé par UserViewSet.login juste après une authentification réussie (mot de passe ou OTP selon la config), avant que la réponse (JSON et/ou cookies JWT) ne soit renvoyée au client. Il permet au projet hôte de brancher des actions personnalisées (audit, notifications, mise à jour de métadonnées, etc.) sans avoir à surcharger la vue.

Arguments envoyés : sender (la classe UserViewSet), request, user.

from django.dispatch import receiver
from forge_auth.signals import user_logged_in

@receiver(user_logged_in)
def on_forge_auth_login(sender, request, user, **kwargs):
    ...

Ce signal est spécifique à forge_auth (et distinct de django.contrib.auth.signals.user_logged_in) car l'authentification se fait via JWT et non via django.contrib.auth.login() / la session Django.

Signal otp_requested

forge_auth.signals.otp_requested est envoyé par UserViewSet.obtain_otp juste après la génération d'un nouveau code OTP, avant que la réponse ne soit renvoyée au client. C'est le point d'extension prévu pour l'envoi effectif du code (SMS, WhatsApp, email...) — voir "Points non automatisés" ci-dessous.

Arguments envoyés : sender (la classe UserViewSet), request, user, otp_token (le code en clair est disponible via otp_token.otp_code).

from django.dispatch import receiver
from forge_auth.signals import otp_requested

@receiver(otp_requested)
def on_forge_auth_otp_requested(sender, request, user, otp_token, **kwargs):
    send_sms(user.phone_number, otp_token.otp_code)

Signal password_reset_requested

forge_auth.signals.password_reset_requested est envoyé par UserViewSet.request_password_reset juste après la génération d'un token de réinitialisation, avant que la réponse ne soit renvoyée au client. Même principe que otp_requested : c'est le point d'extension prévu pour l'envoi effectif du lien/code (email, SMS...) — voir "Points non automatisés" ci-dessous.

Arguments envoyés : sender (la classe UserViewSet), request, user, token (le token en clair, à inclure dans le lien envoyé à l'utilisateur — vérifié ensuite par confirm-password-reset).

from django.dispatch import receiver
from forge_auth.signals import password_reset_requested

@receiver(password_reset_requested)
def on_forge_auth_password_reset_requested(sender, request, user, token, **kwargs):
    send_email(user.email, f"https://example.com/reset?username={user.username}&token={token}")

Signal contact_verification_requested

forge_auth.signals.contact_verification_requested est envoyé par UserViewSet.request_contact_verification, même principe que otp_requested/password_reset_requested.

Arguments envoyés : sender, request, user, field ("email" ou "phone_number"), token.

from django.dispatch import receiver
from forge_auth.signals import contact_verification_requested

@receiver(contact_verification_requested)
def on_forge_auth_contact_verification_requested(sender, request, user, field, token, **kwargs):
    if field == "email":
        send_email(user.email, f"https://example.com/verify-email?token={token}")
    else:
        send_sms(user.phone_number, f"Code de vérification : {token}")

Signal magic_link_requested

forge_auth.signals.magic_link_requested est envoyé par UserViewSet.request_magic_link (actif uniquement si MAGIC_LINK.ENABLED=True), même principe.

Arguments envoyés : sender, request, user, token.

from django.dispatch import receiver
from forge_auth.signals import magic_link_requested

@receiver(magic_link_requested)
def on_forge_auth_magic_link_requested(sender, request, user, token, **kwargs):
    send_email(user.email, f"https://example.com/magic-login?token={token}")

Avertissement sur les migrations

Les migrations fournies (0001_initial, 0002_user_otp_secret_user_status, 0003_otptoken, 0004_...) ont été générées pour la configuration par défaut, c'est-à-dire OPTIONAL_FIELDS = [] (les champs status, otp_secret et profile_photo, ainsi que le modèle OtpToken, existent en base). 0004 ajoute failed_login_attempts/locked_until/profile_photo sur User et les modèles ApiKey, SessionMetadata, LoginAuditLog, TotpDevice, TotpBackupCode, SocialAccount — tous inconditionnels (indépendants de OPTIONAL_FIELDS), sauf profile_photo.

OPTIONAL_FIELDS ne modifie que la classe Python User au chargement de l'application ; il ne régénère pas les migrations. Si vous changez OPTIONAL_FIELDS après avoir appliqué ces migrations sur une base existante, makemigrations détectera un écart (le modèle n'a plus les champs que les migrations ont créés) et vous devrez générer puis appliquer vos propres migrations de suppression. Si vous démarrez un projet neuf avec OPTIONAL_FIELDS déjà fixé, faites-le avant la toute première migrate, ou régénérez les migrations vous-même.

Points non automatisés (à implémenter côté projet hôte)

Ces options de FORGE_AUTH sont validées au démarrage mais ne déclenchent aucune action automatique dans le code fourni :

  • OTP.OTP_CANAL : obtain-otp génère et stocke le code (otp_token.otp_code), mais ne l'envoie nulle part. L'envoi effectif (SMS, WhatsApp, email) est à la charge du projet hôte, via le signal otp_requested (voir plus haut) ou en surchargeant l'action obtain_otp.
  • OTP.OTP_LIFETIME : aucune expiration n'est vérifiée dans verify_otp(). À implémenter si nécessaire (comparaison avec otp_token.updated_at).
  • Envoi du token de réinitialisation de mot de passe (request-password-reset) : signal password_reset_requested, rien ne l'envoie par défaut.
  • Envoi du token de vérification de contact (request-contact-verification) : signal contact_verification_requested, rien ne l'envoie par défaut.
  • Envoi du lien de connexion sans mot de passe (request-magic-link) : signal magic_link_requested, rien ne l'envoie par défaut.

Automatisés (post_migrate ou à la création d'utilisateur, voir signals.py/models.py) :

  • CREDENTIALS_SUPERUSER : un superutilisateur est créé automatiquement au premier migrate si aucun n'existe déjà (receiver create_superuser).
  • GROUPS : les groupes listés sont créés automatiquement au migrate (receiver initialize_groups).
  • GROUP_DEFAULT : assigné automatiquement à tout nouvel utilisateur créé sans groups explicite (UserManager.create_user).
  • ACCOUNT_LOCKOUT : verrouillage/déverrouillage entièrement géré par User.register_failed_login/register_successful_login.

Notes de sécurité

  • OtpToken.verify_otp() retourne toujours True lorsque settings.DEBUG = True, quel que soit le code fourni. Ne déployez jamais avec DEBUG = True.
  • Les cookies JWT (JWT.VIA_HTTP_ONLY) sont posés avec secure=True dès que DEBUG = False. En développement local sans HTTPS, gardez DEBUG = True pour que les cookies soient acceptés par le navigateur.
  • rest_framework_simplejwt.token_blacklist doit être dans INSTALLED_APPS pour que logout, change-password, confirm-password-reset et revoke-session puissent réellement blacklister les refresh tokens (sinon l'appel échoue silencieusement, capturé par un except ImportError/except Exception: pass).
  • /users/ est protégé par IsSelfOrAdmin (voir plus haut) : un utilisateur non-staff ne peut lister, consulter, modifier ou supprimer que son propre compte. La suppression de son propre compte exige en plus le mot de passe courant dans le corps de la requête (DELETE {"password": "..."}) — pas pour le staff supprimant un tiers.
  • login, authenticate-user et verify-otp-and-login vérifient is_active, is_unauthorized et le verrouillage (ACCOUNT_LOCKOUT) avant de délivrer un JWT : un compte désactivé/bloqué/suspendu/supprimé/verrouillé ne peut plus s'authentifier, même avec les bons identifiants.
  • Aucun débit n'est limité par défaut (voir "Throttling") : configurez REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] pour vous protéger du brute force sur login/OTP/reset de mot de passe/vérification d'existence.
  • La force du mot de passe n'est vérifiée (création, change-password, confirm-password-reset) que si AUTH_PASSWORD_VALIDATORS est configuré côté projet hôte ([] par défaut dans Django, donc aucune validation sans configuration explicite — voir "Configuration rapide").
  • change-password et confirm-password-reset blacklistent les refresh tokens existants de l'utilisateur : les autres sessions ouvertes avec l'ancien mot de passe sont invalidées (nécessite rest_framework_simplejwt.token_blacklist, voir ci-dessus).
  • ApiKeyAuthentication n'est pas activée par défaut : sans elle, une clé API créée via create-api-key ne peut authentifier aucune requête (voir "Clés API (M2M)").
  • La connexion sociale fait confiance au fournisseur OIDC configuré (SOCIAL_AUTH) : vérifiez que CLIENT_ID/ISSUER correspondent bien à votre application avant de déployer, une mauvaise configuration accepterait des id_token destinés à une autre application du même fournisseur.

Lancer les tests

uv sync --extra dev
uv run python -m pytest

(python -m pytest plutôt que pytest directement : garantit que le répertoire courant est sur sys.path, nécessaire pour que tests.settings s'importe.)

La configuration de test se trouve dans tests/settings.py et tests/urls.py. Organisation des tests, pour s'y retrouver :

Fichier Couvre
tests/tests.py Endpoints DRF de bout en bout (déclaratif, via le package externe django-forge-testForgeCase/ConfigForgeCase, dépendance dev) : CRUD users/groups, login, logout, refresh, verify-email/phone, session-check.
tests/test_conf.py Validation de ForgeAuthConfig (conf.py) : clés inconnues, types invalides, valeurs par défaut.
tests/test_models.py User, UserManager (dont GROUP_DEFAULT), StatusMixin, OtpToken.
tests/test_backends.py MultiFieldBackend (auth Django classique multi-champs).
tests/test_authentication.py JWTAuthenticationFlexible (JWT via cookie et/ou header).
tests/test_signals.py Signaux user_logged_in, otp_requested, et les receivers post_migrate (create_superuser, initialize_groups).
tests/test_f2fa_views.py Flux F2FA (authenticate-user, verify-otp-and-login) : accès anonyme, échec fermé si OTP désactivé.
tests/test_permissions.py IsSelfOrAdmin (IDOR sur /users/) et hachage du mot de passe sur update().
tests/test_login_security.py Blocage du login (is_active/is_unauthorized) pour les comptes désactivés/bloqués/suspendus/supprimés.
tests/test_password_management.py change-password, request-password-reset, confirm-password-reset.
tests/test_refresh.py refresh accessible sans authentification préalable, synchronisation du cookie access, rotation des refresh tokens.
tests/test_profile_photo.py Champ optionnel profile_photo (upload, validation d'image).
tests/test_contact_verification.py Vérification de contact par token (email/téléphone).
tests/test_account_lockout.py Verrouillage de compte après échecs répétés (ACCOUNT_LOCKOUT).
tests/test_sessions.py Enregistrement/liste/révocation des sessions (SessionMetadata).
tests/test_login_audit.py Écriture et consultation de LoginAuditLog.
tests/test_account_deletion.py Confirmation par mot de passe avant auto-suppression de compte.
tests/test_mfa_totp.py Second facteur TOTP applicatif (setup/confirm/disable) et son intégration au login.
tests/test_magic_link.py Connexion sans mot de passe (magic link).
tests/test_api_keys.py Clés API M2M (ApiKey, ApiKeyAuthentication).
tests/test_social_auth.py Connexion sociale OIDC (forge_auth.social.verify_id_token mocké).
tests/test_throttling.py ForgeAuthScopedRateThrottle : no-op par défaut, applique le débit si configuré.
tests/test_i18n.py Régression sur les messages traduits (gettext_lazy) qui ne doivent jamais crasher (UnboundLocalError sur l'alias _).
tests/test_management_command.py Commande forge_auth_setup : sélections multiples, confirmation finale, détection d'un FORGE_AUTH déjà présent, saisie masquée du mot de passe.
tests/_helpers.py Utilitaires partagés (non collecté par pytest) : voir les docstrings pour les pièges de configuration en cours de test (forge_auth_config.otp_conf/jwt_conf/register_include_in_otp figés au démarrage, non rafraîchis par reset()).

Download files

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

Source Distribution

django_forge_auth-0.2.0.tar.gz (170.6 kB view details)

Uploaded Source

Built Distribution

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

django_forge_auth-0.2.0-py3-none-any.whl (74.3 kB view details)

Uploaded Python 3

File details

Details for the file django_forge_auth-0.2.0.tar.gz.

File metadata

  • Download URL: django_forge_auth-0.2.0.tar.gz
  • Upload date:
  • Size: 170.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_forge_auth-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f087e8bab2b3206bf9e6564b426d9546ef8d515c777229aec8ef88d587d3234c
MD5 e94d8ad21ac1ca0172e7d89e4947fb7f
BLAKE2b-256 34b6f5616a9322cd70b6d6102b8e3b4c4cec444c301484912c56e79e84318cec

See more details on using hashes here.

File details

Details for the file django_forge_auth-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_forge_auth-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 74.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for django_forge_auth-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a553d88a4229d1437028515e520ff6302aa8b7d1357a2b74b97ec6d768481855
MD5 f0b2bb5c841982afcab07d919e472625
BLAKE2b-256 ade32312a056c229a5981413ed9dd2c0749c0f05398e610135df4632d63110a3

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.1

2 files

This release

0.2.0 This release

2 files

0.1.44

2 files

0.1.42

2 files

0.1.41

2 files

0.1.40

2 files

0.1.39

2 files

0.1.38

2 files

0.1.37

2 files

0.1.36

2 files

0.1.35

2 files

0.1.34

2 files

0.1.32

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 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