Comment fonctionne le TOTP : l’algorithme derrière les codes de votre application d’authentification
Plusieurs fois par semaine, vous recopiez un code à 6 chiffres depuis une application d’authentification. Le même code s’affiche sur votre téléphone et correspond à ce que le serveur attend, alors que les deux ne se parlent jamais. Comment le TOTP s’y prend-il ? Le secret partagé produit un nouveau nombre toutes les 30 secondes, et chacune des deux parties le calcule chez elle à partir d’un algorithme déterministe. Rien ne transite par le réseau, et aucun serveur central ne distribue le code.
Le TOTP (Time-based One-Time Password), défini par la RFC 6238, transforme un secret partagé et l’heure courante en un court code numérique : il applique un HMAC sur l’heure puis tronque le résultat. L’authentification à deux facteurs (2FA) tient à ce que les deux parties calculent la même valeur sans jamais l’échanger, et c’est donc l’algorithme qui porte toute la confiance du système.
Ce guide parcourt l’algorithme de bout en bout avec des nombres concrets, puis traite la moitié que la plupart des explications laissent de côté : comment un serveur vérifie réellement un code, et un bilan honnête de ce que la 2FA arrête et de ce qu’elle laisse passer. Vous pouvez calculer un code en direct dans notre générateur TOTP au fil de la lecture.
Le TOTP, concrètement, c’est quoi ?
Le TOTP combine un secret partagé et l’heure courante pour produire un court code qui change à intervalle fixe. L’application d’authentification et le serveur détiennent le même secret, lisent la même horloge et font les mêmes calculs : ils arrivent donc au même code sans jamais le transmettre.
Ce point change tout. Le code n’est jamais envoyé pendant la configuration ; seul le secret l’est, et ensuite chaque partie calcule ses propres codes. Il n’y a presque rien à intercepter sur le réseau : le secret au moment de l’enrôlement, puis les 6 chiffres que l’utilisateur saisit à la connexion. Tout le mécanisme se ramène à trois entrées et une sortie :
| Entrée | Rôle | Valeur typique |
|---|---|---|
| Secret partagé | La clé à longue durée de vie, convenue une fois à l’enrôlement | JBSWY3DPEHPK3PXP (Base32) |
| Pas de temps | Le compteur qui avance | Fenêtre de 30 secondes |
| Sortie | Le court code dérivé des deux précédents | 324550 |
Le secret est presque toujours écrit en Base32 (les lettres A–Z et les chiffres 2–7), parce que cet alphabet ignore la casse et passe sans dommage par l’impression, la saisie au clavier ou l’encodage dans un QR code. On enrôle un secret en scannant un URI otpauth://, que vous pouvez transformer en QR code d’authentification, ou en tapant la chaîne Base32 à la main.
TOTP, HOTP, SMS et passkeys : situer la 2FA
Le TOTP n’est qu’une option parmi d’autres, et pour bien choisir il faut voir le reste. La relation à retenir en une ligne : le TOTP, c’est du HOTP dont le compteur est remplacé par le nombre de pas de temps écoulés depuis l’epoch Unix. Le reste se joue surtout sur trois critères : la résistance au hameçonnage, le confort d’usage et l’infrastructure que le mécanisme exige.
| Mécanisme | Pilote | Durée de vie du code | Résistant au hameçonnage ? | Réseau requis ? | Usage typique |
|---|---|---|---|---|---|
| HOTP (RFC 4226) | Compteur incrémental | Jusqu’à utilisation | Non | Non | Tokens matériels, ancien |
| TOTP (RFC 6238) | Heure courante | ~30 secondes | Non | Non (après enrôlement) | Applications d’authentification |
| OTP par SMS | Le serveur envoie un code | Quelques minutes | Non | Oui (cellulaire) | Repli grand public |
| Validation push | Demande envoyée à un appareil | Par requête | Partiellement | Oui | 2FA via application |
| Passkey / FIDO2 | Défi à clé publique | Par requête | Oui (lié à l’origine) | Oui | Comptes modernes |
Quelques lignes de force se dégagent. Le TOTP et le HOTP fonctionnent hors ligne une fois l’enrôlement fait, ce qui les rend fiables et respectueux de la vie privée ; mais aucun des deux ne résiste seul au hameçonnage, puisqu’une fausse page convaincante peut demander le code et le relayer. Le SMS ajoute un canal réseau, donc une surface d’attaque supplémentaire. Les passkeys, eux, ferment la faille du hameçonnage en liant l’identifiant à l’origine du site, et c’est vers eux que l’industrie se dirige. Le TOTP tient le milieu : fiable, présent partout et gratuit, ce qui suffit à expliquer pourquoi il reste aussi répandu.
Comment fonctionne l’algorithme TOTP, étape par étape
L’algorithme complet tient en quatre étapes. Nous les déroulerons une à une avec le secret de test de la RFC JBSWY3DPEHPK3PXP et une heure Unix fixée à 1700000000, pour que chaque nombre soit reproductible.
- Décoder le secret Base32 en octets de clé bruts.
- Calculer le compteur de pas de temps à partir de l’heure Unix courante.
- Appliquer un HMAC sur le compteur avec la clé secrète.
- Tronquer le condensé pour obtenir un code à 6 chiffres.
Étape 1 — Décoder le secret Base32 en octets
Chaque caractère Base32 porte 5 bits. Le décodeur regroupe ces caractères pour reformer des octets de 8 bits. Le secret JBSWY3DPEHPK3PXP se décode ainsi en 10 octets bruts 48 65 6c 6c 6f 21 de ad be ef. C’est ce tableau d’octets, et non la chaîne imprimable, qui sert de clé HMAC.
Étape 2 — Calculer le compteur de pas de temps
Le compteur est le nombre de pas de temps entiers écoulés depuis un point de départ : T = floor((unixTime − T0) / period). Les valeurs par défaut de la RFC sont T0 = 0 (l’epoch Unix) et period = 30. Avec unixTime = 1700000000, on obtient T = floor(1700000000 / 30) = 56666666. Cet entier est ensuite encodé sur 8 octets en big-endian : 00 00 00 00 03 60 aa 2a. Le compteur ne bouge qu’au passage d’une nouvelle fenêtre de 30 secondes ; le code reste donc identique le temps d’une fenêtre, puis saute à la valeur suivante.
Étape 3 — Appliquer un HMAC sur le compteur avec le secret
L’algorithme applique un HMAC-SHA1 sur le compteur de 8 octets, avec les octets du secret comme clé. Le HMAC est une fonction à clé et à sens unique : sans le secret, on ne peut ni remonter au condensé ni en fabriquer un qui passe, et c’est ce qui empêche de falsifier un code. Pour nos entrées, le condensé fait 20 octets : 1d 70 6e 94 1a c7 6b 6d 4a 46 dd 6f af a4 5f e3 35 11 bf 86.
Étape 4 — Troncature dynamique vers un code à 6 chiffres (RFC 4226)
Un condensé de 20 octets est trop long à recopier : la troncature dynamique de la RFC 4226 en extrait donc un nombre. Le quartet de poids faible du dernier octet sert de décalage : le dernier octet est 0x86, son quartet de poids faible vaut 6, le décalage est donc 6. On lit 4 octets à partir de là (6b 6d 4a 46), on masque le bit de poids fort du premier pour rester positif, et l’entier obtenu est 1802324550. Réduit modulo 10^6 et complété par des zéros : 1802324550 % 1000000 = 324550. Voilà le code que votre application affiche pour ce secret à cet instant.
Voici l’algorithme en JavaScript, avec l’API native Web Crypto du navigateur et aucune dépendance. Chaque commentaire rattache un bloc à l’une des quatre étapes ci-dessus :
// TOTP per RFC 6238 — SHA-1, 6 digits, 30s period (the defaults).
async function generateTotp(base32Secret, unixTime = Date.now() / 1000) {
// Step 1: decode the Base32 secret (A-Z, 2-7) to raw key bytes.
const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const ch of base32Secret.replace(/=+$/, '').toUpperCase()) {
bits += alpha.indexOf(ch).toString(2).padStart(5, '0');
}
const keyBytes = new Uint8Array(
bits.match(/.{8}/g).map((b) => parseInt(b, 2)));
// Step 2: counter = number of 30s steps since the epoch (8-byte big-endian).
let counter = Math.floor(unixTime / 30);
const msg = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
msg[i] = counter & 0xff;
counter = Math.floor(counter / 256);
}
// Step 3: HMAC-SHA1 the counter with the secret key.
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const hmac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg));
// Step 4: dynamic truncation (RFC 4226) -> 6-digit code.
const offset = hmac[hmac.length - 1] & 0x0f;
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) |
(hmac[offset + 2] << 8) | hmac[offset + 3];
return (binary % 1_000_000).toString().padStart(6, '0');
}
const code = await generateTotp('JBSWY3DPEHPK3PXP', 1700000000);
console.log(code); // -> "324550"
Le même algorithme en Python, avec la seule bibliothèque standard (hmac et struct) :
import base64, hmac, hashlib, struct, time
def totp(secret, for_time=None, period=30, digits=6, digest='sha1'):
if for_time is None:
for_time = time.time()
# Step 1: Base32-decode the secret to raw key bytes.
key = base64.b32decode(secret.upper())
# Step 2: counter = number of time steps since the epoch (8-byte big-endian).
counter = int(for_time // period)
msg = struct.pack(">Q", counter)
# Step 3: HMAC the counter with the secret.
h = hmac.new(key, msg, digest).digest()
# Step 4: dynamic truncation (RFC 4226) -> N-digit code.
offset = h[-1] & 0x0f
binary = ((h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff))
return str(binary % (10 ** digits)).zfill(digits)
print(totp('JBSWY3DPEHPK3PXP', 1700000000)) # -> 324550
Les deux implémentations affichent 324550 pour notre heure fixe, et toutes deux retrouvent les vecteurs de test officiels de la RFC 6238 (le vecteur SHA-1 à T = 59 donne par exemple 94287082). Si vous passez de SHA-1 à SHA-256 ou SHA-512, ou si vous changez le nombre de chiffres, le vérificateur d’en face doit faire exactement les mêmes choix, faute de quoi les codes ne concorderont jamais.
Vérifier un code TOTP côté serveur
Générer un code n’est que la moitié du système. L’autre moitié, c’est le serveur qui décide d’accepter ou non les 6 chiffres que l’utilisateur vient de taper, et c’est là que se logent les arbitrages de sécurité délicats.
Le serveur ne stocke pas les codes. Il stocke le secret, et à la connexion il recalcule le code attendu à partir de ce secret et de l’heure courante, puis compare. L’ennui, c’est la dérive d’horloge : l’appareil de l’utilisateur et le serveur s’accordent rarement à la seconde près, donc une comparaison d’égalité stricte rejetterait les codes saisis tout près d’une limite de fenêtre. D’où une petite fenêtre de validation. Acceptez le pas courant et un pas de part et d’autre, c’est-à-dire les codes des compteurs T−1, T et T+1. Une fenêtre plus large absorbe plus de dérive mais élargit aussi la surface de devinette ; la fenêtre 1 (une tolérance de ±30 secondes) reste donc l’équilibre habituel. C’est cette tolérance de ±1 pas que vous retrouvez dans l’onglet Vérifier du générateur.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyTotp(secret, code, { window = 1, period = 30, digits = 6 } = {}) {
const counter = Math.floor(Date.now() / 1000 / period);
const submitted = Buffer.from(code);
// Check the current step and ±window steps for clock drift.
for (let i = -window; i <= window; i++) {
const expected = Buffer.from(totpAt(secret, counter + i, digits));
// Constant-time compare so timing can't leak a partial match.
if (expected.length === submitted.length &&
timingSafeEqual(expected, submitted)) {
return counter + i; // matched step — store it to block replay
}
}
return false;
}
Deux détails de plus font passer ce code de « fonctionne » à « sûr ». D’abord la prévention du rejeu : gardez le dernier compteur accepté pour chaque utilisateur et refusez tout code issu d’un pas inférieur ou égal, de sorte qu’un code intercepté une fois ne resserve pas dans la même fenêtre. C’est la raison pour laquelle verifyTotp renvoie le pas correspondant au lieu d’un simple true. Ensuite la limitation de débit : un code à 6 chiffres n’est qu’une valeur parmi un million, et la fenêtre de ±1 en rend trois valides à chaque instant ; sans frein, un attaquant peut balayer l’espace par force brute. Verrouillez le compte, ou allongez le délai après quelques échecs. Enfin, le secret est une clé à longue durée de vie : chiffrez-le au repos, tenez-le à l’écart du contrôle de version et traitez-le comme un mot de passe. Générez en parallèle de solides codes de récupération, pour le jour où un appareil disparaîtra.
Ce que le TOTP protège, et ce qu’il ne protège pas
Le TOTP vaut bien mieux que le mot de passe seul, mais il ne fait pas de miracle, et les pages marketing ont tendance à passer vite sur ses limites. Voici le partage honnête.
| Ce que le TOTP arrête | Ce que le TOTP n’arrête PAS |
|---|---|
| Mots de passe fuités ou réutilisés | Hameçonnage en temps réel / adversaire au milieu |
| Bourrage d’identifiants | Logiciel malveillant qui lit le secret sur un appareil |
| Force brute distante sur le mot de passe | Procédures de récupération de compte faibles qui contournent la 2FA |
| Une fuite de base de données n’exposant que des empreintes de mots de passe | (cela exige d’autres défenses) |
Les gains sont réels. Puisque la connexion réclame désormais un code que seul le secret peut produire, un mot de passe fuité ne suffit plus, et le bourrage d’identifiants comme la force brute distante tombent du même coup. Si votre base de données fuite mais que les secrets TOTP y sont chiffrés au repos, l’attaquant ne peut toujours pas fabriquer de codes.
Les limites le sont tout autant. Un proxy de hameçonnage en temps réel (une page « adversaire au milieu ») peut montrer à l’utilisateur une réplique parfaite, capter le code à la volée et le rejouer vers le vrai site dans la même fenêtre. Le TOTP n’a aucun moyen de voir que le code a été saisi au mauvais endroit. Un logiciel malveillant qui siphonne le secret sur l’appareil le bat à plate couture, et une procédure de récupération « j’ai perdu ma 2FA » mal ficelée le contourne tout aussi bien. Une confusion revient souvent, autant la lever : l’échange de carte SIM casse les codes à usage unique par SMS, pas le TOTP ; le TOTP n’a aucun canal accroché à un numéro de téléphone, il n’y a donc rien à détourner.
Vers quoi se tourner ensuite ? Les passkeys et FIDO2/WebAuthn sont liés à l’origine, donc résistants au hameçonnage par construction : l’identifiant refuse simplement de s’authentifier auprès du mauvais domaine. Voyez le TOTP comme un solide cran au-dessus du mot de passe, disponible partout, mais pas comme le point d’arrivée. Il s’emboîte avec le reste de votre pile d’authentification : voyez les bonnes pratiques de sécurité JWT pour la couche du jeton de session, qui suppose une connexion déjà vérifiée, et le hachage de mots de passe (bcrypt vs Argon2) pour la couche du mot de passe au repos, que la 2FA vient compléter.
Pièges courants lors de l’implémentation du TOTP
La plupart des bogues de TOTP ne viennent pas de l’algorithme, figé par la RFC, mais du câblage autour. Voici ceux qui piègent le plus souvent.
- Dérive de l’horloge serveur. Si le serveur ne tourne pas sous NTP, son « maintenant » dérive de celui de l’appareil de l’utilisateur et les codes cessent de concorder pour tout le monde. Activez la synchronisation horaire réseau sur chaque hôte.
- Secrets en clair ou versionnés. Un secret posé dans un fichier de configuration committé sur git est une porte dérobée qui ne se referme jamais. Rangez-le chiffré dans un gestionnaire de secrets, jamais dans le contrôle de version.
- Pas de protection contre le rejeu. Si vous acceptez un code sans noter le pas auquel il correspond, le même code repasse dans sa fenêtre. Gardez le dernier pas utilisé par utilisateur et refusez toute réutilisation.
- Une fenêtre trop large ou trop étroite. Trop large, elle multiplie les codes devinables et affaiblit la sécurité ; trop étroite, elle recale des utilisateurs honnêtes au moindre décalage. La fenêtre 1 reste l’équilibre habituel.
- Paramètres incohérents. Si l’enrôlement inscrit SHA-256 et 8 chiffres dans l’URI
otpauth://alors que le vérificateur suppose SHA-1 et 6 chiffres, aucun code ne passera jamais. Lisez l’algorithme, le nombre de chiffres et la période dans l’URI, et appliquez-les des deux côtés. - Pas de codes de secours ou de récupération. Quand un téléphone disparaît, la seule porte de retour est une voie de récupération. Émettez des codes de récupération dès la configuration, et rendez-les aussi robustes que le compte le mérite ; le raisonnement sur l’entropie des mots de passe vaut aussi pour les secrets de récupération.
FAQ
Le TOTP est-il infaillible face au hameçonnage ?
Non. Le TOTP arrête les mots de passe fuités et la force brute distante, mais un proxy de hameçonnage en temps réel peut afficher une fausse page de connexion, capter le code à la volée et le relayer vers le vrai site dans la même fenêtre de 30 secondes. Pour résister au hameçonnage, il faut des passkeys ou FIDO2, qui lient l’identifiant à l’origine du site.
Le TOTP est-il plus sûr que la 2FA par SMS ?
Oui. Les codes SMS passent par le réseau cellulaire et peuvent être interceptés via un échange de carte SIM ou des attaques SS7, et ils reposent sur la sécurité de votre opérateur. Le TOTP n’a aucun canal accroché à un numéro de téléphone et ne transmet jamais le code, il n’y a donc rien à capter en transit. Le secret ne s’échange qu’une fois, à la configuration.
Que se passe-t-il si je perds mon téléphone ou mon application d’authentification ?
Il faut une sauvegarde prévue à l’avance. Trois possibilités : des codes de récupération enregistrés au moment où vous activez la 2FA, un second appareil enrôlé avec le même secret, ou le secret Base32 d’origine rangé en lieu sûr. Sans rien de tout cela, perdre l’appareil revient à se faire verrouiller hors du compte.
Comment un serveur vérifie-t-il un code TOTP ?
Il recalcule le code attendu à partir du secret partagé et de l’heure courante, puis compare le code soumis au pas de temps courant et à un pas de chaque côté, pour absorber la dérive d’horloge. Il note aussi quel pas a correspondu, pour empêcher de rejouer le même code, et limite le débit des tentatives pour couper court à la force brute.
Pourquoi les codes TOTP changent-ils toutes les 30 secondes ?
Trente secondes, c’est la période par défaut de la RFC 6238 : assez longue pour lire et taper le code sans se presser, assez courte pour qu’un code capté par un attaquant expire presque aussitôt. Certains systèmes prennent 60 secondes, valeur que l’URI otpauth:// enregistre pour que le vérificateur s’y cale.
Deux appareils peuvent-ils partager un même secret TOTP ?
Oui. N’importe quel appareil qui détient le même secret Base32 avec une horloge synchronisée produit des codes identiques, puisque l’algorithme est déterministe. C’est précisément le ressort des sauvegardes multi-appareils des applications d’authentification, et c’est aussi pourquoi le secret doit rester privé : qui le copie peut générer tous les codes à venir.
Le TOTP est-il la même chose que Google Authenticator ?
Non. Le TOTP est l’algorithme ouvert défini par la RFC 6238 ; Google Authenticator, Authy et 1Password sont des applications qui l’implémentent. La norme étant commune, toute application conforme marche avec tout service qui utilise le TOTP, sans aucun enfermement chez un fournisseur.
Conclusion
L’essentiel tient en quelques lignes :
- Le TOTP transforme un secret partagé et l’heure courante en code, par un HMAC suivi d’une troncature.
- Chaque partie calcule le code de son côté ; il ne part jamais sur le réseau.
- Vérifiez avec une fenêtre de ±1 pas, plus une protection contre le rejeu et une limitation de débit.
- Il bloque les attaques sur les mots de passe, mais pas le hameçonnage en temps réel, faille que les passkeys viennent fermer.
- Gardez les horloges serveur synchronisées avec NTP, et le secret chiffré et privé.
Pour voir l’algorithme cracher de vrais nombres et essayer votre propre fenêtre de vérification, ouvrez le générateur TOTP / 2FA : il calcule, configure et vérifie des codes entièrement dans votre navigateur, le secret ne quittant jamais votre appareil.