Aller au contenu

Auth/User avancee

Auth/User est la brique optionnelle de Forge pour representer une identite utilisateur moderne sans transformer le framework en application metier. Elle fournit des contrats Python, des helpers explicites et des SQL visibles que les projets peuvent adopter progressivement.

Vue d'ensemble

Auth/User repond a la question : qui est l'utilisateur ? RBAC repond a la question : qu'a-t-il le droit de faire ? Les deux briques peuvent etre reliees par user_roles, mais restent separees.

Principes :

  • pas d'ORM impose ;
  • pas de modele utilisateur metier riche impose ;
  • SQL optionnels visibles dans mvc/models/sql/ ;
  • aucune route login/reset/MFA/OIDC generee automatiquement ;
  • aucune ecriture en base cachee dans les helpers de contrat ;
  • aucune permission stockee dans users ;
  • logique applicative et politique de securite finale cote projet.

Les modules Auth/User disponibles couvrent aujourd'hui :

  • utilisateur local ;
  • mot de passe Argon2id ;
  • session utilisateur ;
  • tokens a usage limite ;
  • verification email ;
  • reset password ;
  • MFA TOTP, recovery codes, challenge et revalidation ;
  • OIDC local avec state, nonce et PKCE ;
  • pont Auth/User vers RBAC ;
  • administration CLI utilisateurs ;
  • audit Auth ;
  • rate limit Auth.

Contrat utilisateur

AuthUser est le contrat minimal d'un utilisateur authentifiable.

from dataclasses import dataclass
from typing import Any

@dataclass(frozen=True)
class AuthUser:
    id: int
    email: str
    password_hash: str
    is_active: bool = True
    created_at: Any | None = None
    updated_at: Any | None = None

API :

  • normalize_auth_user(data) -> AuthUser
  • validate_auth_user_contract(data)
  • is_valid_auth_user(user) -> bool
  • InvalidAuthUserError

id doit etre strictement positif, email non vide, password_hash non vide et is_active booleen. Forge ne demande pas de nom, avatar, telephone, adresse, profil proprietaire ou statut metier.

users.sql

forge auth:init cree ou preserve :

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    email_verified_at DATETIME NULL,
    last_login_at DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

email_verified_at et last_login_at sont des colonnes utiles aux flux applicatifs. Forge ne les met pas a jour automatiquement.

Mot de passe

Forge fournit le hachage et la verification de mot de passe avec Argon2id.

from core.auth import hash_password, verify_password, password_needs_rehash

password_hash = hash_password("mot-de-passe")
ok = verify_password("mot-de-passe", password_hash)
needs = password_needs_rehash(password_hash)

API :

  • hash_password(password)
  • verify_password(password, password_hash) -> bool
  • password_needs_rehash(password_hash) -> bool
  • validate_new_password(password)
  • InvalidNewPasswordError

Le mot de passe clair n'est jamais stocke. Le reset password valide seulement une regle minimale de nouveau mot de passe : chaine non vide et longueur minimale. Les politiques plus complexes appartiennent aux applications.

Session utilisateur

La session Auth/User stocke uniquement l'identifiant utilisateur local sous une cle de session interne. Elle ne stocke ni email, ni password_hash, ni objet AuthUser complet.

from core.auth import authenticate_user, login_user, logout_user

user = authenticate_user(email, password, load_user_by_email)

if user is not None:
    login_user(request, user)

logout_user(request)

API :

  • authenticate_user(email, password, user_loader) -> AuthUser | None
  • login_user(request, user) -> None
  • logout_user(request) -> None
  • get_authenticated_user_id(request) -> int | None
  • current_user(request, user_loader) -> AuthUser | None
  • is_authenticated(request) -> bool
  • login_required

authenticate_user appelle un loader fourni par l'application. Il refuse les utilisateurs inactifs et retourne None pour les echecs normaux. Il ne fait pas de requete SQL lui-meme.

current_user recharge l'utilisateur via un loader applicatif. Si la session est absente, invalide, si le loader retourne None, ou si l'utilisateur est inactif, le resultat est None.

@login_required protege une fonction controleur. Il retourne 401 par defaut ou peut rediriger si redirect_to est fourni.

Tokens Auth

AuthToken represente un jeton securise a usage limite. Le token brut est donne une seule fois a l'application ; seul son hash est stockable.

from core.auth import generate_auth_token, hash_auth_token, verify_auth_token

raw_token = generate_auth_token()
token_hash = hash_auth_token(raw_token)
ok = verify_auth_token(raw_token, token_hash)

Structure :

@dataclass(frozen=True)
class AuthToken:
    user_id: int
    purpose: str
    token_hash: str
    expires_at: datetime
    used_at: datetime | None = None
    created_at: datetime | None = None

API :

  • generate_auth_token(nbytes=32)
  • hash_auth_token(token)
  • verify_auth_token(token, token_hash)
  • token_expires_at(minutes=60, now=None)
  • is_token_expired(expires_at, now=None)
  • is_token_usable(token_record, purpose=None, now=None)
  • normalize_auth_token(data)
  • validate_auth_token_contract(data)
  • is_valid_auth_token(token_record)

auth_tokens.sql

CREATE TABLE IF NOT EXISTS auth_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    purpose VARCHAR(80) NOT NULL,
    token_hash CHAR(64) NOT NULL UNIQUE,
    expires_at DATETIME NOT NULL,
    used_at DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_auth_tokens_user_purpose (user_id, purpose),
    INDEX idx_auth_tokens_expires_at (expires_at),
    CONSTRAINT fk_auth_tokens_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

used_at permet a l'application de marquer un token comme consomme. Forge ne met pas cette colonne a jour automatiquement.

Verification email

La verification email s'appuie sur les tokens generiques.

from core.auth import (
    EMAIL_VERIFICATION_PURPOSE,
    create_email_verification_token,
    verify_email_verification_token,
    email_verification_timestamp,
    is_email_verified,
)

raw_token, token_record = create_email_verification_token(user_id=1)
ok = verify_email_verification_token(raw_token, token_record)

Responsabilites de l'application :

  • stocker token_record.token_hash dans auth_tokens ;
  • envoyer le token brut dans un lien ;
  • appeler verify_email_verification_token au retour ;
  • renseigner users.email_verified_at ;
  • renseigner auth_tokens.used_at.

Forge ne fournit pas d'envoi automatique d'email, route de confirmation, controleur ou template.

Mot de passe oublie

Le reset password se fait en deux etapes : creation/verifications du token, puis production d'un nouveau hash.

from core.auth import create_password_reset_token, reset_password_with_token

raw_token, token_record = create_password_reset_token(user_id=1)
result = reset_password_with_token(raw_token, token_record, "nouveau-mot-de-passe")

if result is not None:
    # users.password_hash = result.password_hash
    # auth_tokens.used_at = result.used_at
    pass

API :

  • PASSWORD_RESET_PURPOSE
  • create_password_reset_token(user_id, minutes=30, now=None)
  • verify_password_reset_token(token, token_record, now=None)
  • password_reset_timestamp(now=None)
  • create_password_reset_request(user, minutes=30, now=None)
  • reset_password_with_token(token, token_record, new_password, now=None)
  • PasswordResetRequest
  • PasswordResetResult

PasswordResetResult contient user_id, password_hash et used_at. Il ne contient jamais le mot de passe clair ni le token brut. Forge ne fait aucune ecriture DB automatique.

MFA

Forge fournit le socle MFA par briques :

  • contrat et table des facteurs ;
  • TOTP ;
  • codes de recuperation ;
  • challenge MFA a la connexion ;
  • revalidation MFA pour actions sensibles.

Facteurs MFA

from core.auth import AuthMfaFactor, normalize_mfa_factor, is_mfa_enabled

AuthMfaFactor decrit user_id, factor_type, secret_hash, status, label, confirmed_at, last_used_at, created_at et updated_at.

Statuts :

  • pending
  • active
  • disabled

Types :

  • totp
  • recovery

auth_mfa_factors.sql

CREATE TABLE IF NOT EXISTS auth_mfa_factors (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    factor_type VARCHAR(40) NOT NULL,
    secret_hash VARCHAR(255) NOT NULL,
    status VARCHAR(40) NOT NULL DEFAULT 'pending',
    label VARCHAR(120) NULL,
    confirmed_at DATETIME NULL,
    last_used_at DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_auth_mfa_factors_user_id (user_id),
    INDEX idx_auth_mfa_factors_user_status (user_id, status),
    CONSTRAINT fk_auth_mfa_factors_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Le champ historique s'appelle secret_hash. Pour TOTP, il contient le secret stocke par l'application. Il doit etre protege en base. Forge ne doit jamais afficher ce secret, ni l'inclure dans un audit ou une tentative rate limit.

TOTP

API principale :

  • generate_totp_secret()
  • totp_provisioning_uri(secret, account_name, issuer="Forge")
  • create_totp_factor(user_id, secret, label=None)
  • confirm_totp_factor(factor, code, secret, now=None)
  • verify_totp_code(secret, code, now=None)

Le secret brut est necessaire pour verifier les codes. Sa persistance securisee reste une responsabilite applicative.

Codes de recuperation

API :

  • create_recovery_codes(user_id, count=10)
  • generate_recovery_code()
  • hash_recovery_code(code)
  • verify_recovery_code(code, code_hash)
  • consume_recovery_code(code, code_record, now=None)

create_recovery_codes retourne des codes bruts a afficher une seule fois et des records hashables. Les codes bruts ne doivent jamais etre stockes.

auth_mfa_recovery_codes.sql

CREATE TABLE IF NOT EXISTS auth_mfa_recovery_codes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    code_hash CHAR(64) NOT NULL UNIQUE,
    used_at DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_auth_mfa_recovery_codes_user_id (user_id),
    INDEX idx_auth_mfa_recovery_codes_used_at (used_at),
    CONSTRAINT fk_auth_mfa_recovery_codes_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Challenge MFA

API :

  • start_mfa_challenge(request, user, now=None)
  • has_pending_mfa_challenge(request, max_age_minutes=10, now=None)
  • get_mfa_challenge_user_id(request)
  • verify_mfa_challenge(request, code, factors, recovery_codes=(), now=None)
  • clear_mfa_challenge(request)
  • MfaChallengeResult

Le challenge stocke seulement user_id et un timestamp en session. Il ne connecte pas l'utilisateur automatiquement.

Revalidation MFA

API :

  • require_recent_mfa(request, max_age_minutes=15, now=None)
  • verify_mfa_revalidation(request, code, factors, recovery_codes=(), now=None)
  • mark_mfa_revalidated(request, user_id, now=None)
  • has_recent_mfa_revalidation(request, max_age_minutes=15, now=None)
  • clear_mfa_revalidation(request)
  • MfaRevalidationResult

La revalidation sert aux actions sensibles deja authentifiees : changement de mot de passe, action admin, export sensible, etc.

OIDC

OIDC est fourni comme socle local et generique. Forge gere les structures, la preparation du flux et la validation locale du callback, mais ne fait pas encore l'echange reseau de code contre token et ne valide pas de JWT.

Fournisseur et client

from core.auth import OidcProvider, OidcClientConfig

OidcProvider contient le nom du fournisseur et ses endpoints. OidcClientConfig contient le fournisseur, client_id, client_secret, redirect_uri et scopes.

API :

  • normalize_oidc_provider
  • validate_oidc_provider_contract
  • is_valid_oidc_provider
  • normalize_oidc_client_config
  • validate_oidc_client_config_contract
  • is_valid_oidc_client_config

State, nonce et PKCE

API :

  • generate_oidc_state()
  • generate_oidc_nonce()
  • generate_pkce_code_verifier()
  • pkce_code_challenge_s256(code_verifier)
  • start_oidc_login(request, config, now=None)
  • has_pending_oidc_login(request, max_age_minutes=10, now=None)
  • validate_oidc_callback(request, provider_name, state, code, max_age_minutes=10, now=None)
  • clear_oidc_login(request)

start_oidc_login stocke en session uniquement les donnees temporaires necessaires : fournisseur, state, nonce, code verifier, redirect URI et timestamp.

Identite externe et compte local

API :

  • OidcExternalIdentity
  • AuthOidcAccount
  • normalize_oidc_external_identity
  • create_oidc_account_link(user_id, identity, now=None)
  • oidc_identity_key(identity)
  • oidc_account_matches_identity(account, identity)

AuthOidcAccount relie un utilisateur local a un couple stable provider + subject. L'email OIDC peut changer et ne doit pas servir de cle stable.

auth_oidc_accounts.sql

CREATE TABLE IF NOT EXISTS auth_oidc_accounts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    provider VARCHAR(120) NOT NULL,
    subject VARCHAR(255) NOT NULL,
    email VARCHAR(255) NULL,
    email_verified BOOLEAN NULL,
    name VARCHAR(255) NULL,
    last_login_at DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_auth_oidc_accounts_provider_subject (provider, subject),
    INDEX idx_auth_oidc_accounts_user_id (user_id),
    CONSTRAINT fk_auth_oidc_accounts_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Forge ne stocke aucun access_token, refresh_token ou id_token dans cette table. L'ancien SQL auth_oidc_identities.sql reste preserve pour compatibilite avec l'etat de developpement existant.

Auth/User vers RBAC

user_roles est le pont optionnel entre les utilisateurs locaux et les roles RBAC existants.

from core.auth import (
    create_auth_user_role,
    get_user_permissions,
    get_user_role_ids,
    user_has_permission,
    require_user_permission,
)

Flux de resolution :

session Auth/User -> user_id -> user_roles -> roles -> role_permissions -> permissions

user_roles.sql

CREATE TABLE IF NOT EXISTS user_roles (
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, role_id),
    INDEX idx_user_roles_user_id (user_id),
    INDEX idx_user_roles_role_id (role_id),
    CONSTRAINT fk_user_roles_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE CASCADE,
    CONSTRAINT fk_user_roles_role_id
        FOREIGN KEY (role_id)
        REFERENCES roles(id)
        ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

require_user_permission("article.edit") lit l'utilisateur Auth/User connecte et interroge le resolver user_roles -> roles -> permissions. Il retourne 401 si aucun utilisateur Auth/User n'est connecte et 403 si la permission manque.

Difference avec le RBAC historique

@require_permission(...), fourni par core.security.rbac, reste le decorateur historique. Il lit les permissions deja presentes dans request.permissions ou dans la session RBAC historique. Il ne lit pas automatiquement user_roles.

require_user_permission(...), fourni par core.auth, est le decorateur serveur pour Auth/User + RBAC.

can(...) dans Jinja est un helper d'affichage. Il peut utiliser le contexte Auth/User injecte par BaseController.render(..., request=request) ou le mode historique. Il ne remplace jamais une protection serveur.

Administration CLI

Les commandes Auth/User disponibles dans cette copie de Forge sont :

forge auth:init
forge auth:doctor
forge auth:status
forge auth:list-sql
forge auth:user:create --email admin@example.com --password-prompt
forge auth:user:list
forge auth:user:show --email admin@example.com
forge auth:user:disable --email user@example.com
forge auth:user:enable --email user@example.com
forge auth:user:password --email user@example.com --password-prompt
forge auth:user:role:add --email user@example.com --role admin
forge auth:user:role:remove --email user@example.com --role admin
forge auth:user:roles --email user@example.com

forge auth:init cree ou preserve les SQL optionnels suivants :

  • users.sql
  • auth_tokens.sql
  • auth_mfa_factors.sql
  • auth_mfa_recovery_codes.sql
  • auth_oidc_accounts.sql
  • auth_oidc_identities.sql
  • user_roles.sql
  • auth_audit_log.sql
  • auth_rate_limit_attempts.sql

La commande ne cree aucun utilisateur, aucun token, aucun facteur MFA, aucun compte OIDC, aucun role utilisateur, aucun audit et aucune tentative rate limit. Elle n'applique pas non plus le SQL.

Les commandes d'administration utilisateur n'affichent aucun mot de passe, hash, token ou secret MFA. Elles s'appuient sur la configuration projet (config.py, env/dev, variables DB_APP_*) et sur la table optionnelle users.

Les commandes de roles utilisateur manipulent uniquement la table optionnelle user_roles :

  • auth:user:role:add attribue a un utilisateur un role RBAC deja existant ;
  • auth:user:role:remove retire cette association ;
  • auth:user:roles liste les roles attribues.

Elles ne creent aucun utilisateur, aucun role et aucune permission. Les roles et permissions restent definis par le RBAC (roles, permissions, role_permissions). Le parametre --role accepte un id numerique, un slug ou un nom de role existant.

Audit Auth

AuthAuditEvent represente un evenement d'audit Auth/User lisible et stockable.

from core.auth import AUTH_EVENT_LOGIN_SUCCESS, create_auth_audit_event

event = create_auth_audit_event(
    event_type=AUTH_EVENT_LOGIN_SUCCESS,
    user_id=1,
    ip_address="192.0.2.10",
    user_agent="Mozilla/5.0",
    metadata={"method": "password"},
)

API :

  • AuthAuditEvent
  • normalize_auth_audit_event(data)
  • validate_auth_audit_event_contract(data)
  • is_valid_auth_audit_event(event)
  • create_auth_audit_event(...)
  • sanitize_auth_audit_metadata(metadata)

Evenements standards :

  • login.success
  • login.failed
  • logout
  • password_reset.requested
  • password_reset.completed
  • email.verified
  • mfa.challenge.success
  • mfa.challenge.failed
  • mfa.revalidation.success
  • mfa.revalidation.failed
  • user.disabled
  • user.enabled
  • user.password_changed
  • user_role.added
  • user_role.removed
  • oidc.account_linked

metadata est nettoye avant stockage applicatif. Les cles sensibles retirees incluent password, password_hash, token, raw_token, access_token, refresh_token, id_token, secret, secret_hash, totp_secret, recovery_code et code_verifier.

auth_audit_log.sql

CREATE TABLE IF NOT EXISTS auth_audit_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(120) NOT NULL,
    user_id INT NULL,
    actor_user_id INT NULL,
    ip_address VARCHAR(45) NULL,
    user_agent VARCHAR(255) NULL,
    metadata_json TEXT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_auth_audit_log_event_type (event_type),
    INDEX idx_auth_audit_log_user_id (user_id),
    INDEX idx_auth_audit_log_actor_user_id (actor_user_id),
    INDEX idx_auth_audit_log_created_at (created_at),
    CONSTRAINT fk_auth_audit_log_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE SET NULL,
    CONSTRAINT fk_auth_audit_log_actor_user_id
        FOREIGN KEY (actor_user_id)
        REFERENCES users(id)
        ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Forge ne branche pas automatiquement l'audit dans login/reset/MFA/OIDC/admin.

Rate limit Auth

Le rate limit Auth/User represente des tentatives d'actions sensibles et calcule une decision anti-bruteforce a partir des tentatives chargees par l'application.

from core.auth import (
    AUTH_RATE_LIMIT_LOGIN,
    AuthRateLimitRule,
    check_auth_rate_limit,
    create_auth_rate_limit_attempt,
)

rule = AuthRateLimitRule(
    action=AUTH_RATE_LIMIT_LOGIN,
    max_attempts=5,
    window_seconds=900,
)

decision = check_auth_rate_limit(
    action=AUTH_RATE_LIMIT_LOGIN,
    key=email,
    attempts=load_attempts(email),
    rule=rule,
)

API :

  • AuthRateLimitAttempt
  • AuthRateLimitRule
  • AuthRateLimitDecision
  • normalize_rate_limit_key(value)
  • normalize_auth_rate_limit_attempt(data)
  • validate_auth_rate_limit_attempt_contract(data)
  • is_valid_auth_rate_limit_attempt(attempt)
  • normalize_auth_rate_limit_rule(data)
  • validate_auth_rate_limit_rule_contract(data)
  • is_valid_auth_rate_limit_rule(rule)
  • create_auth_rate_limit_attempt(...)
  • check_auth_rate_limit(...)

Actions standards :

  • login
  • password_reset
  • mfa_challenge
  • mfa_revalidation
  • oidc_callback

check_auth_rate_limit compte uniquement les echecs success=False pour le couple action + key dans la fenetre window_seconds. Les succes, les autres actions, les autres cles et les tentatives hors fenetre sont ignores. Si la limite est atteinte, la decision contient retry_after_seconds.

auth_rate_limit_attempts.sql

CREATE TABLE IF NOT EXISTS auth_rate_limit_attempts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    action VARCHAR(120) NOT NULL,
    rate_key VARCHAR(255) NOT NULL,
    ip_address VARCHAR(45) NULL,
    user_id INT NULL,
    success BOOLEAN NOT NULL DEFAULT FALSE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_auth_rate_limit_action_key (action, rate_key),
    INDEX idx_auth_rate_limit_created_at (created_at),
    INDEX idx_auth_rate_limit_user_id (user_id),
    CONSTRAINT fk_auth_rate_limit_user_id
        FOREIGN KEY (user_id)
        REFERENCES users(id)
        ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

La colonne SQL s'appelle rate_key, car key peut etre ambigu. Forge ne stocke aucun mot de passe, token ou secret dans les tentatives et ne branche pas automatiquement cette protection dans les flux Auth.

Flux recommandes

Login classique sans MFA

from core.auth import authenticate_user, login_user

user = authenticate_user(email, password, load_user_by_email)

if user is None:
    return invalid_credentials_response()

login_user(request, user)
return redirect("/dashboard")

L'application peut ensuite mettre a jour users.last_login_at, stocker un audit login.success, ou enregistrer une tentative rate limit reussie si elle le souhaite.

Login avec MFA

from core.auth import authenticate_user, is_mfa_enabled, start_mfa_challenge, login_user

user = authenticate_user(email, password, load_user_by_email)

if user is None:
    return invalid_credentials_response()

factors = load_mfa_factors(user.id)

if is_mfa_enabled(factors):
    start_mfa_challenge(request, user)
    return show_mfa_form()

login_user(request, user)

Puis, dans l'etape MFA :

from core.auth import verify_mfa_challenge, login_user

result = verify_mfa_challenge(
    request,
    code,
    factors=load_mfa_factors(user_id),
    recovery_codes=load_recovery_codes(user_id),
)

if result is None:
    return invalid_mfa_response()

user = load_user_by_id(result.user_id)
login_user(request, user)

Forge ne persiste pas last_used_at ou used_at automatiquement.

Reset password

raw_token, token_record = create_password_reset_token(user_id=1)
# stocker token_record.token_hash, envoyer raw_token

result = reset_password_with_token(raw_token, token_record, new_password)

if result is not None:
    # users.password_hash = result.password_hash
    # auth_tokens.used_at = result.used_at
    pass

Verification email

raw_token, token_record = create_email_verification_token(user_id=1)
# stocker token_record.token_hash, envoyer raw_token

if verify_email_verification_token(raw_token, token_record):
    verified_at = email_verification_timestamp()
    # users.email_verified_at = verified_at
    # auth_tokens.used_at = verified_at

OIDC local jusqu'au callback

login_request = start_oidc_login(request, config)
return redirect(login_request.authorization_url)

Au callback :

callback = validate_oidc_callback(request, "google", state, code)

if callback is None:
    return invalid_oidc_response()

# Echanger callback.code contre les tokens cote application.
# Valider le JWT et le nonce cote application.
# Creer ou retrouver AuthOidcAccount cote application.

Protection route avec require_user_permission

from core.auth import require_user_permission

@require_user_permission("articles.edit")
def edit_article(request, article_id):
    ...

Pour l'affichage :

{% if can("articles.edit") %}
  <a href="/articles/{{ article.id }}/edit">Modifier</a>
{% endif %}

Le helper Jinja masque l'action ; le decorateur serveur protege la route.

Action sensible avec revalidation MFA

from core.auth import require_recent_mfa

def change_password(request):
    if not require_recent_mfa(request):
        return redirect("/mfa/revalidate")
    ...

Puis :

result = verify_mfa_revalidation(
    request,
    code,
    factors=load_mfa_factors(user_id),
    recovery_codes=load_recovery_codes(user_id),
)

if result is None:
    return invalid_mfa_response()

Rate limit autour d'un login applicatif

from core.auth import (
    AUTH_RATE_LIMIT_LOGIN,
    AuthRateLimitRule,
    check_auth_rate_limit,
    create_auth_rate_limit_attempt,
)

rule = AuthRateLimitRule(
    action=AUTH_RATE_LIMIT_LOGIN,
    max_attempts=5,
    window_seconds=900,
)

decision = check_auth_rate_limit(
    action=AUTH_RATE_LIMIT_LOGIN,
    key=email,
    attempts=load_login_attempts(email),
    rule=rule,
)

if not decision.allowed:
    return too_many_attempts(decision.retry_after_seconds)

user = authenticate_user(email, password, load_user_by_email)
attempt = create_auth_rate_limit_attempt(
    action=AUTH_RATE_LIMIT_LOGIN,
    key=email,
    ip_address=request.ip,
    success=user is not None,
)
# stocker attempt dans auth_rate_limit_attempts

Limites restantes

Forge ne fournit pas encore :

  • interface HTML admin utilisateurs ;
  • routes Auth generees automatiquement ;
  • middleware global Auth/User ;
  • envoi automatique d'emails ;
  • echange reseau OIDC code -> token ;
  • validation cryptographique JWT ;
  • WebAuthn / passkeys ;
  • SAML ;
  • OAuth multi-provider avance ;
  • multi-tenant Auth/User ;
  • consultation CLI ou HTML du journal d'audit ;
  • consultation CLI ou HTML des tentatives rate limit ;
  • politiques complexes d'organisation ou de delegation admin.

Ces limites sont volontaires. Forge fournit des briques explicites ; les applications choisissent leurs flux, leurs routes, leur persistance et leurs politiques metier.