Aller au contenu principal

Une authentification sans mot de passe bien conçue

· 13 minutes de lecture
Dave Rapin
Dave Rapin
Fondateur chez Curling IO
Mise à jour : 1er octobre 2026

Cet article présente notre exploration initiale de Gleam pour Curling IO v3. Nous avons finalement choisi Rust et TypeScript. Pourquoi nous avons choisi Rust pour la version 3 explique cette décision et ce que nous avons conservé de cette exploration.

Curling IO fonctionne sans mot de passe depuis la version 2. Aucun mot de passe à retenir ou à voler, aucun parcours de réinitialisation. Vous entrez votre adresse courriel, nous vous envoyons un code de connexion de courte durée et vous êtes connecté. Cette approche fonctionne bien depuis plus de dix ans. Pour la version 3, nous la conservons tout en corrigeant certains irritants et en ajoutant la prise en charge de plusieurs adresses courriel.

Mais parlons d’abord des raisons de cette décision controversée.

Pourquoi pas des mots de passe?​

Selon l’idée reçue, les mots de passe sont la « vraie » façon de s’authentifier, et les liens de connexion sont un raccourci. Nous pensons le contraire. Les mots de passe sont le raccourci et créent plus de problèmes qu’ils n’en règlent. Nous ne sommes pas seuls de cet avis : Slack, Notion, Medium et Substack utilisent tous les liens de connexion sans mot de passe comme principale méthode d’authentification.

Les mots de passe sont réutilisés. Les études montrent régulièrement que la plupart des gens réutilisent le même mot de passe sur plusieurs sites. Lorsqu’un de ces sites subit une fuite, les attaquants essaient ces identifiants partout ailleurs. Ce risque n’est pas théorique. Il se concrétise constamment et constitue la principale façon dont les comptes sont compromis. En tant que développeur d’application, vous ne contrôlez pas ce que vos utilisateurs font sur les autres sites, mais vous héritez du risque.

Les mots de passe sont victimes d’hameçonnage. Une fausse page de connexion convaincante peut recueillir des identifiants à grande échelle. Les liens de connexion éliminent le mot de passe réutilisable qu’un attaquant pourrait voler et essayer sur d’autres sites. Ils n’éliminent pas l’hameçonnage : une fausse page peut encore convaincre quelqu’un de transmettre un code de courte durée que l’attaquant utilise avant son expiration. L’authentification par courriel ne résiste pas à l’hameçonnage. Une authentification liée à l’origine du site, comme les clés d’accès, offre cette protection.

Les mots de passe exigent tout autant de vérification par courriel. On l’oublie souvent : même avec des mots de passe, il faut vérifier l’adresse courriel de l’utilisateur. Sinon, n’importe qui peut créer un compte avec l’adresse d’une autre personne. Il faut donc créer le même parcours de vérification que pour les liens de connexion, puis y ajouter la gestion des mots de passe. Le courriel reste nécessaire, avec une couche supplémentaire de mots de passe.

La connexion par courriel seulement est un compromis. Un mot de passe accompagné d’un second facteur indépendant peut offrir une protection absente de la connexion par courriel seulement. Nous avons choisi les liens de connexion parce que les membres des clubs se connectent souvent seulement quelques fois par saison. Éliminer la gestion des mots de passe simplifie ce parcours. Ce choix accorde davantage de confiance à la sécurité du compte courriel du membre. Il n’équivaut pas à une authentification multifacteur.

Les mots de passe ajoutent des éléments à protéger. Ils exigent un hachage sûr (bcrypt/argon2), un parcours « mot de passe oublié » (qui est lui-même un lien de connexion), une validation de la robustesse du mot de passe, une protection contre les attaques par force brute sur le formulaire de connexion et un stockage sûr. Les liens de connexion exigent eux aussi une limitation des tentatives et une gestion des jetons, mais l’ensemble à protéger est plus petit. Aucun identifiant secret à stocker à long terme, aucune politique de mots de passe à appliquer et aucun parcours de mot de passe oublié à créer, puisque le parcours de connexion est le parcours de récupération.

« Et les gestionnaires de mots de passe? » Ils sont excellents. Ils aident à prévenir la réutilisation des mots de passe, et le remplissage automatique lié à l’adresse du site aide à éviter de transmettre un mot de passe à un faux site. Mais la plupart des utilisateurs ne s’en servent pas. Dans un club de curling, où les membres se connectent quelques fois par saison, leur demander de configurer et de maintenir un gestionnaire de mots de passe est irréaliste. Les liens de connexion s’adaptent à leurs habitudes.

Un lien de connexion est un code de courte durée envoyé par courriel. Le modèle de sécurité repose sur une hypothèse simple : si vous pouvez lire les courriels envoyés à une adresse, vous contrôlez cette adresse. Les parcours de réinitialisation de mot de passe reposent sur la même hypothèse, mais nous éliminons l’intermédiaire.

Voici ce qui rend l’implémentation sûre :

Des codes aléatoires. Les codes de connexion sont générés aléatoirement, expirent rapidement et ne servent qu’une fois. Leur résistance aux tentatives de deviner dépend à la fois du nombre de codes possibles et de la fréquence des tentatives permises.

Une limitation stricte des tentatives. Les tentatives d’authentification sont fortement limitées sur plusieurs fenêtres temporelles superposées. Deviner un jeton par force brute est pratiquement impossible. La syntaxe use de Gleam vérifie chaque fenêtre à tour de rôle. Si une limite est dépassée, la vérification s’arrête et retourne le délai avant la prochaine tentative, sans vérifier les autres fenêtres :

pub fn check_verification_attempt(
limiter: RateLimiter,
email: String,
now: Int,
) -> Result(Nil, Int) {
let key = key_prefix <> string.lowercase(email)
use _ <- result.try(check(limiter, key, attempts_per_minute, 60, now))
use _ <- result.try(check(limiter, key, attempts_per_15_minutes, 900, now))
use _ <- result.try(check(limiter, key, attempts_per_hour, 3600, now))
check(limiter, key, attempts_per_day, 86_400, now)
}

Une courte durée de validité. Chaque code expire rapidement. Après vérification, son empreinte est effacée de la base de données. Une fois utilisé, le code ne peut plus servir.

Une comparaison en temps constant. Nous cherchons d’abord la ligne de l’adresse courriel, puis comparons l’empreinte du jeton soumis à celle du jeton stocké avec crypto.secure_compare de Gleam. La comparaison prend le même temps, peu importe où se trouve la première différence. Cela empêche les attaques temporelles qui pourraient révéler des correspondances partielles. Si le jeton actuel ne correspond pas, nous vérifions le précédent pour retourner une erreur précise qui invite l’utilisateur à chercher un courriel plus récent. Le jeton précédent ne permet pas de se connecter :

let hashes_match =
crypto.secure_compare(
bit_array.from_string(submitted_hash),
bit_array.from_string(stored_hash),
)

case hashes_match {
True ->
case row.token_expires_at {
Some(expires_at) if expires_at > now ->
complete_verification(conn, row, now)
_ -> Error(ExpiredToken)
}
False ->
case row.previous_token_hash {
Some(prev_hash) -> {
let prev_match =
crypto.secure_compare(
bit_array.from_string(submitted_hash),
bit_array.from_string(prev_hash),
)
case prev_match {
True -> Error(SupersededToken)
False -> Error(InvalidToken)
}
}
None -> Error(InvalidToken)
}
}

Chaque branche retourne une variante d’erreur précise, et le compilateur nous oblige à toutes les traiter. Aucun cas limite oublié. Le jeton précédent ne donne jamais accès au compte. Il sert seulement à afficher une erreur plus utile qu’un message générique de jeton non valide.

Aucun stockage d’identifiants secrets. Notre base de données ne contient aucun mot de passe. En cas de fuite, les attaquants obtiennent des empreintes de jetons de courte durée. Dans une base de mots de passe, chaque empreinte est une cible pour le cassage hors ligne.

Un compte courriel compromis peut donner accès à Curling IO. Plusieurs services à mot de passe utilisent aussi le courriel pour la récupération, mais des facteurs supplémentaires et des contrôles de récupération peuvent changer ce risque. Il est essentiel de protéger le compte courriel. Une courte expiration n’empêche pas un attaquant de relayer un code en temps réel.

Pour notre usage, les liens de connexion éliminent la réutilisation de mots de passe, les essais d’identifiants volés sur d’autres sites et les attaques qui essaient un même mot de passe sur de nombreux comptes Curling IO. Ils simplifient la connexion et la récupération. Ils exigent tout de même une gestion soignée des jetons, une limitation des tentatives et une présentation honnête des risques de l’authentification par courriel.

Les leçons de la version 2​

Le parcours de connexion de la version 2 est simple : entrez votre adresse courriel, recevez un code, entrez le code et vous êtes connecté. Cela fonctionne. Mais après dix ans et des centaines de milliers d’utilisateurs, quelques irritants sont apparus.

Une adresse, une identité. Dans la version 2, votre adresse courriel est votre identité. Une autre adresse donne accès à un autre compte. Cette situation a causé beaucoup de confusion. Un gestionnaire s’inscrit avec son adresse professionnelle, puis essaie de se connecter sur son téléphone avec son adresse personnelle et trouve un compte vide. Nous recevions des demandes de soutien pour des « inscriptions manquantes » qui correspondaient en réalité à deux comptes de la même personne.

Des erreurs peu utiles. Ce problème revenait souvent. Un curleur demandait un lien de connexion, attendait une minute ou deux sans recevoir le courriel, puis en demandait un autre. Le premier courriel était généralement simplement retardé. De nombreux logiciels de courriel ne vérifient l’arrivée de nouveaux messages que toutes les 5 à 15 minutes. Lorsque le premier courriel arrivait enfin et que le curleur cliquait sur le lien, il obtenait une erreur générique de jeton non valide, car la deuxième demande avait remplacé la première. Il se demandait s’il avait fait une faute de frappe, réessayait, se perdait et contactait le soutien.

La version 3 ajoute plusieurs adresses courriel​

C’est le grand ajout. Les utilisateurs peuvent maintenant associer plusieurs adresses courriel vérifiées à leur compte :

Ajouter une adresse. Dans la page du compte, entrez une nouvelle adresse courriel. Nous envoyons un code de vérification par le même mécanisme que pour la connexion.

La vérifier. Entrez le code. L’adresse est maintenant liée à votre compte et marquée comme vérifiée. Les adresses non vérifiées ne peuvent servir à rien.

La définir comme adresse principale. Toute adresse vérifiée peut devenir l’adresse principale. Celle-ci apparaît partout sur la plateforme : tableaux de bord d’administration, listes d’équipe et communications par courriel. Un clic suffit pour changer d’adresse principale.

La retirer. Les adresses secondaires peuvent être retirées. Vous ne pouvez pas retirer votre adresse principale sans en choisir une autre d’abord, ni retirer votre dernière adresse vérifiée, car il en faut au moins une pour vous connecter.

Se connecter avec n’importe laquelle. C’est le principal avantage. Après avoir ajouté et vérifié plusieurs adresses, vous pouvez utiliser n’importe laquelle pour retrouver le même compte, les mêmes inscriptions et le même historique. Plus de comptes en double parce que vous avez utilisé une autre adresse.

Le fonctionnement interne​

Chaque ligne d’adresse courriel suit quelques champs importants :

email -- globally unique, one owner max
user_id -- nullable; not set until verified for first-time logins
token_hash -- pending verification token
previous_token_hash -- the token that was replaced (for superseded detection)
verified_at -- null until verified

Cette seule structure gère la vérification de connexion et la propriété des adresses courriel.

Grâce à cette conception, la prise en charge de plusieurs adresses n’a presque rien coûté de plus. Le parcours de vérification utilisé pour la connexion vérifie aussi les adresses des utilisateurs déjà connectés. La seule différence est que l’utilisateur est déjà défini lorsque vous ajoutez une adresse à votre compte existant.

Détecter les jetons remplacés​

Vous vous rappelez le problème des courriels retardés de la version 2? Un curleur demande un lien, ne le voit pas tout de suite, en demande un autre, puis clique sur le premier lorsqu’il arrive enfin. La version 3 gère bien cette situation.

Lorsqu’un nouveau lien est demandé, nous déplaçons d’abord la valeur actuelle de token_hash dans previous_token_hash au lieu de simplement l’écraser. Si quelqu’un essaie ensuite l’ancien jeton, nous pouvons faire la différence :

pub type AuthError {
InvalidToken // not found or already used
SupersededToken // replaced by a newer token
ExpiredToken // past expiry
UserDisabled // account disabled
DatabaseError(sqlight.Error)
}

Un SupersededToken produit un message précis : « Ce code a été remplacé. Cherchez un courriel plus récent ou demandez un nouveau lien. » Nous guidons ainsi les utilisateurs plutôt que de les laisser perplexes.

Nous utilisons cette approche partout dans la version 3. Les types union de Gleam permettent de modéliser explicitement tous les états d’erreur possibles, et le compilateur nous oblige à tous les traiter. Aucun cas limite oublié, aucune erreur générique qui masque tous les cas.

OAuth (Google et Facebook)​

Les liens de connexion ne sont pas le seul moyen d’entrer. La connexion Google et Facebook fonctionne en parallèle pour les personnes qui préfèrent l’authentification unique en un clic.

Le parcours OAuth utilise un sous-domaine d’authentification central (auth.curling.io) pour gérer les retours, car les deux fournisseurs exigent une URI de redirection fixe. Après la vérification de l’identité par le fournisseur, nous cherchons l’adresse dans la même table que celle des liens de connexion. Si l’adresse existe et possède un utilisateur, nous le connectons. Sinon, nous créons l’utilisateur et une ligne d’adresse vérifiée, puisque le fournisseur a déjà vérifié les adresses OAuth.

La logique de résolution de l’utilisateur reste la même, peu importe la méthode d’authentification.

Concevoir sa propre authentification​

Écrire son propre système d’authentification est généralement une mauvaise idée. Des bibliothèques éprouvées comme Devise (Ruby), NextAuth (JavaScript) et django-allauth (Python) existent pour de bonnes raisons. Des années d’utilisation réelle et d’audits de sécurité les ont renforcées. Si vos technologies disposent d’une bibliothèque d’authentification mature, utilisez-la.

Nous n’avons pas trouvé de bibliothèque Gleam qui convenait à nos besoins précis, mais nous n’avons pas conçu le système en vase clos. Nous avons étudié en détail les modules de Devise (Lockable, Timeoutable, Trackable, Confirmable) et nous en sommes servis comme liste de vérification des exigences d’un système d’authentification de production. Chacune de nos décisions de sécurité, de la comparaison en temps constant à la limitation des tentatives par couches et à la prévention de l’énumération des adresses courriel, s’appuyait sur les leçons difficiles apprises par ces bibliothèques au cours de la dernière décennie.

La suite​

L’authentification est une fondation. La vraie valeur de plusieurs adresses courriel apparaît toutefois dans ses liens avec les inscriptions, la gestion des équipes, les permissions d’administration et l’historique des paiements. À mesure que nous développons ces fonctions, une résolution claire de l’utilisateur entre plusieurs adresses simplifiera de nombreux parcours pénibles dans la version 2.

Nous envisageons aussi une authentification à deux facteurs facultative pour les administrateurs de club, par application d’authentification ou SMS, pour les organisations qui veulent imposer une couche de sécurité supplémentaire aux comptes ayant un accès administratif.


Cet article est le troisième de la série sur les fondations de Curling IO. À suivre : les tâches en arrière-plan sans le poids des dépendances.