Quand on développe un espace client pour du B2B — suivi de projet, consultation de factures, échange de documents — le premier réflexe c'est de coller un système de login classique : email + mot de passe, page de connexion, récupération de mot de passe, sessions, tokens. Sauf que dans la vraie vie, ça pose un paquet de problèmes.
Le problème avec les mots de passe en B2B
Vos clients sont des professionnels. Ils ont déjà 47 mots de passe. Ils ne vont pas retenir celui de votre portail qu'ils utilisent une fois par mois pour valider un devis. Résultat concret que j'ai mesuré chez un client :
- 60% des clients n'activent jamais leur compte portail
- Parmi ceux qui l'activent, 40% utilisent "Mot de passe oublié" à chaque connexion
- Le support passe 3h/semaine à gérer des problèmes d'accès portail
- Certains clients demandent carrément qu'on leur envoie les documents par email plutôt que de se connecter
C'est absurde. Le portail est censé simplifier la vie, et il la complique.
J'ai vu des entreprises dépenser 5 000 à 10 000 € en développement d'un système d'authentification complet (inscription, validation email, reset mot de passe, gestion de sessions, protection bruteforce) pour un portail que leurs clients utilisent 2 fois par mois. C'est disproportionné. Et puis il y a le support : chaque mot de passe oublié génère un email ou un appel. Multipliez ça par 60 clients et vous comprenez pourquoi la personne au support en a marre.
L'alternative : les liens signés HMAC
Le principe est simple : au lieu de demander au client de se connecter, on lui envoie un lien unique qui contient son identité, chiffrée et signée. Quand il clique, il accède directement à son espace, sans mot de passe.
C'est exactement le même principe que les liens de désinscription dans les emails, les liens de partage Dropbox/Google Drive, ou les liens de réinitialisation de mot de passe. Sauf qu'ici, c'est le mode d'accès principal.
Comment ça marche techniquement
On encode les informations du client (ID, périmètre d'accès, date d'expiration) dans un payload, puis on signe ce payload avec une clé secrète côté serveur via HMAC-SHA256 :
import hmac
import hashlib
import json
import base64
from datetime import datetime, timedelta
from django.conf import settings
def generer_payload(client_id, scope='portal', duree_jours=90):
# Genere un payload signe pour l acces portail.
data = {
'cid': client_id,
'scope': scope,
'exp': (datetime.utcnow() + timedelta(days=duree_jours)).isoformat(),
'iat': datetime.utcnow().isoformat(),
}
payload = base64.urlsafe_b64encode(
json.dumps(data, separators=(',', ':')).encode()
).decode().rstrip('=')
return payload
def signer(payload):
# Signe un payload avec HMAC-SHA256.
signature = hmac.new(
settings.SECRET_KEY.encode(),
payload.encode(),
hashlib.sha256,
).hexdigest()[:32] # Tronqué pour des URLs plus courtes
return signature
def generer_lien_portail(client_id):
# Genere l URL complete d acces au portail client.
payload = generer_payload(client_id)
signature = signer(payload)
return f"https://votresite.fr/portal/{payload}/{signature}/"
Côté vérification :
def verifier_acces(payload, signature):
# Verifie la signature et l expiration d un lien portail.
# Vérification de la signature
signature_attendue = signer(payload)
if not hmac.compare_digest(signature, signature_attendue):
return None, "Signature invalide"
# Décodage du payload
padding = 4 - len(payload) % 4
if padding != 4:
payload += '=' * padding
try:
data = json.loads(base64.urlsafe_b64decode(payload))
except (json.JSONDecodeError, Exception):
return None, "Payload corrompu"
# Vérification expiration
expiration = datetime.fromisoformat(data['exp'])
if datetime.utcnow() > expiration:
return None, "Lien expiré"
return data, None
Intégration Django : la vue portail
L'implémentation dans Django est propre. On a une URL avec deux segments (payload et signature), et une vue qui vérifie l'accès avant de servir le contenu :
# urls.py
from django.urls import path
urlpatterns = [
path('portal/<str:payload>/<str:signature>/', views.portal_home, name='portal_home'),
path('portal/<str:payload>/<str:signature>/devis/<int:devis_id>/',
views.portal_devis, name='portal_devis'),
]
# views.py
from django.http import HttpResponseForbidden
from django.shortcuts import render, get_object_or_404
def portal_home(request, payload, signature):
data, erreur = verifier_acces(payload, signature)
if erreur:
return HttpResponseForbidden(f"Accès refusé : {erreur}")
client = get_object_or_404(Client, pk=data['cid'])
# Le scope limite ce que le client peut voir
devis = client.devis.all() if 'devis' in data.get('scope', '') else []
factures = client.factures.all() if 'factures' in data.get('scope', '') else []
return render(request, 'portal/home.html', {
'client': client,
'devis': devis,
'factures': factures,
'payload': payload,
'signature': signature, # Pour construire les liens internes
})
Avantages en production
Après 8 mois d'utilisation chez un client (prestataire de services IT, 60 clients actifs) :
- Taux d'utilisation du portail : 94% (vs 40% avec l'ancien système à mot de passe)
- Zéro ticket support lié à des problèmes d'accès
- Temps d'accès moyen : 3 secondes (clic sur le lien dans l'email → portail affiché)
- Aucune base de mots de passe à protéger (un vecteur d'attaque en moins)
Le client envoie les liens portail dans ses emails de suivi de projet. Le destinataire clique, il voit son espace. Pas de création de compte, pas de mot de passe, pas de "première connexion".
Un détail intéressant : on a mesuré le temps entre l'envoi du lien et la première consultation du portail. Avec l'ancien système à mot de passe, le délai moyen était de 48h (le temps que le client se motive pour créer son compte). Avec les liens signés, c'est 12 minutes en moyenne. Le client reçoit l'email, clique, consulte. L'immédiateté change tout dans le suivi de projet.
On a aussi intégré des liens signés dans les SMS de notification. Le client reçoit "Votre devis est prêt : [lien]". Il clique sur son téléphone, il voit le devis, il valide. Tout ça sans jamais taper un mot de passe sur un écran de mobile (ce qui est une UX atroce, on est d'accord).
Sécurité : questions légitimes
La première réaction de tout le monde, c'est "mais c'est pas sécurisé !". Décomposons :
Et si quelqu'un intercepte le lien ?
Même risque qu'un lien de reset de mot de passe. C'est pour ça qu'on utilise HTTPS (le lien est chiffré en transit) et qu'on met une expiration. Un lien qui expire après 90 jours, c'est plus sécurisé qu'un mot de passe "Entreprise2024!" qui ne change jamais.
Et si quelqu'un bruteforce les signatures ?
HMAC-SHA256 avec une clé de 50+ caractères, tronqué à 32 hex. L'espace de recherche est de 16^32, soit 3.4 × 10^38 combinaisons. À 1 milliard de tentatives par seconde, il faudrait 10^22 années pour tomber dessus. On est tranquilles.
Et le transfert de lien ?
Un client peut transférer son lien à un collègue. C'est un feature, pas un bug. Dans un contexte B2B, c'est souvent souhaitable : le lien donne accès au projet du client, pas à un compte personnel. Si la directrice financière veut montrer une facture à son DAF, elle forwarde l'email. Pas besoin de créer un deuxième compte.
Si on veut limiter ça, on peut ajouter une vérification d'IP au premier accès, ou un scope restreint par lien.
Révocation
On peut révoquer un lien en ajoutant un nonce (identifiant unique) dans le payload et en maintenant une liste de nonces révoqués :
class LienRevoque(models.Model):
nonce = models.CharField(max_length=32, unique=True, db_index=True)
raison = models.CharField(max_length=200)
date_revocation = models.DateTimeField(auto_now_add=True)
def verifier_acces(payload, signature):
# ... vérification signature et expiration ...
if LienRevoque.objects.filter(nonce=data.get('nonce', '')).exists():
return None, "Lien révoqué"
return data, None
Stateless vs stateful : pourquoi c'est important
Le gros avantage de cette approche, c'est qu'elle est stateless. Pas de session côté serveur, pas de table de tokens en base de données, pas de cookie. Toute l'information est dans l'URL elle-même.
Conséquences pratiques :
- Scalabilité : pas besoin de session store partagé (Redis, memcached). N'importe quel serveur derrière un load balancer peut vérifier le lien.
- Simplicité : pas de modèle "Session" ou "Token" à maintenir, pas de tâche de nettoyage des sessions expirées.
- Résilience : un redémarrage serveur ne déconnecte personne (il n'y a personne de "connecté").
- Cache-friendly : les réponses peuvent être mises en cache par URL si nécessaire.
Quand ne PAS utiliser cette approche
Soyons honnêtes, les liens signés ne conviennent pas à tout :
- Applications avec actions sensibles (virement bancaire, modification de données critiques) : il faut un 2FA
- Portails avec beaucoup d'utilisateurs individuels et des rôles différents : un vrai système d'auth est plus adapté
- Applications grand public B2C : les utilisateurs s'attendent à un compte
- Conformité stricte (santé, finance réglementée) : les auditeurs veulent voir de l'authentification forte
Pour du B2B classique — suivi de projet, consultation de devis/factures, échange de documents — c'est la meilleure approche que j'ai trouvée. Les clients sont contents, le support est content, le dev est content.
Détails d'implémentation avancés
Quelques patterns supplémentaires que j'ai ajoutés en production et qui font la différence :
Liens multi-scope. Un même client peut avoir plusieurs liens avec des permissions différentes. Le directeur a un lien avec scope "full" (devis + factures + documents + discussions). Le comptable a un lien "compta" (factures uniquement). Le chef de projet a un lien "projet" (avancement + documents). Chaque lien est indépendant et révocable séparément.
Tracking d'ouverture. À chaque accès, on enregistre : date/heure, IP, user-agent, pages consultées. Ça permet de savoir si le client a consulté le devis qu'on lui a envoyé (utile pour les relances commerciales), combien de temps il a passé sur chaque page, et quel device il utilise (important pour l'optimisation responsive).
# Middleware de tracking portail
class PortalTrackingMiddleware:
def __call__(self, request):
response = self.get_response(request)
if hasattr(request, 'portal_client'):
PortalAccess.objects.create(
client=request.portal_client,
path=request.path,
ip=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
status_code=response.status_code,
)
return response
Renouvellement automatique. Quand un lien arrive à 15 jours de l'expiration, le système en génère un nouveau et l'envoie par email au client. Le client n'a rien à faire, il reçoit juste un nouveau lien avec une nouvelle durée de validité. L'ancien lien continue de fonctionner jusqu'à son expiration.
Migration depuis un système à mot de passe
Si vous avez déjà un portail avec authentification classique et que vous voulez migrer vers des liens signés, voici la procédure que j'ai suivie chez un client :
Phase 1 (2 semaines) : implémenter le système de liens signés en parallèle de l'authentification existante. Les deux coexistent. Quand un client se connecte avec son mot de passe, il voit un bandeau : "Facilitez-vous la vie : recevez un lien d'accès direct par email".
Phase 2 (1 mois) : tous les nouveaux clients reçoivent uniquement des liens signés. Pas de création de compte. Les anciens clients continuent avec leur mot de passe mais reçoivent aussi les liens par email.
Phase 3 (2 mois) : on désactive progressivement l'authentification par mot de passe. D'abord pour les clients inactifs depuis 6+ mois (ils ne s'en rendront pas compte), puis pour tous. Les rares clients qui utilisaient encore leur mot de passe reçoivent un email explicatif avec leur nouveau lien.
Sur les 60 clients concernés, on a reçu 2 questions ("pourquoi je ne peux plus me connecter ?") et zéro plainte. La plupart n'ont même pas remarqué le changement — ils cliquaient déjà sur les liens dans les emails plutôt que de taper l'URL du portail.
Variantes et extensions
Quelques idées d'extensions que j'ai implémentées ou envisagées :
- QR code : le lien signé encodé en QR code, imprimé sur les devis papier. Le client scanne avec son téléphone et accède à la version numérique. Pratique pour les secteurs qui utilisent encore du papier (BTP, artisanat).
- Liens éphémères : pour des actions sensibles (validation d'un devis, signature électronique), on génère un lien qui expire après 1h et une seule utilisation. Sécurité maximale.
- Multi-destinataire : un lien avec un scope "viewer" qui permet à n'importe qui de consulter (mais pas de modifier). Utile pour partager l'état d'avancement d'un projet avec des parties prenantes externes.
- Webhooks de notification : quand un client accède à son portail, déclencher un webhook vers votre CRM ou votre outil de gestion de projet. Ça permet au commercial de savoir que le client a consulté son devis et de relancer au bon moment.
L'implémentation de ces extensions est simple parce que le système de base est simple. C'est un des avantages d'une architecture stateless : chaque fonctionnalité est une couche indépendante qui ne touche pas au coeur du mécanisme de signature. Ajouter le tracking n'a pris que 2h, ajouter les QR codes 4h, et les liens éphémères une demi-journée.
Aller plus loin
Si vous développez un espace client et que cette approche vous intéresse, je peux vous accompagner sur la conception et l'implémentation. C'est exactement le type d'application métier sur mesure que je développe. Détails sur mes prestations de développement applicatif ici.