Aller au contenu
Facteur

Playwright · 2FA

Le code arrive par email, ou il se calcule

Un parcours à double authentification bloque une suite de tests à deux endroits : le code envoyé par email, que le test ne peut pas lire, et celui de l’application d’authentification, qu’il ne peut pas générer. Les deux se règlent, et pas de la même façon.

Le code reçu par email

Même mécanique que pour une inscription : la requête part avant le clic et répond à l’arrivée du message.

connexion-2fa.spec.tsts
import { expect, test } from '@playwright/test';

const KEY = process.env.FACTEUR_API_KEY!;
const INBOX = 'j3k9x2mq';

/** hasOtp vise le code extrait, pas un texte qui le cite. */
const attendreLeCode = (sentTo: string) =>
  fetch('https://api.facteur.eu/v1/messages/search', {
    method: 'POST',
    headers: {
      authorization: `Bearer ${KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ inbox: INBOX, sentTo, hasOtp: true, wait: 30_000 }),
  }).then((response) => response.json());

test('la connexion demande le code reçu par email', async ({ page }) => {
  const tag = `login-${Date.now()}`;
  const arrive = attendreLeCode(tag);

  await page.goto('/connexion');
  await page.getByLabel('Email').fill(`${tag}@${INBOX}.inbox.facteur.eu`);
  await page.getByRole('button', { name: 'Continuer' }).click();

  const { messages } = await arrive;

  // Six chiffres, extraits à la réception.
  await page.getByLabel('Code').fill(messages[0].otp);
  await expect(page.getByText('Bienvenue')).toBeVisible();
});
  • Un critère qui vise le code, pas le texte

    Demander « le message qui porte un code » évite de retomber sur un email qui cite un ancien code dans son historique.

  • Le code vit dans son propre champ

    Six chiffres extraits à la réception. Votre test compare une valeur, il ne cherche pas un motif dans du HTML.

Le code d’une application d’authentification

Celui-là, personne ne peut vous l’envoyer : il se calcule à partir du secret que votre écran d’activation affiche. Vingt lignes de Node, aucune dépendance, et le téléphone reste dans le tiroir.

totp.tsts
import { createHmac } from 'node:crypto';

const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

/** Le secret que votre écran d'activation affiche, en base32. */
function decode(secret: string): Buffer {
  let bits = 0;
  let value = 0;
  const out: number[] = [];
  for (const c of secret.replace(/=+$/, '').toUpperCase()) {
    value = (value << 5) | BASE32.indexOf(c);
    bits += 5;
    if (bits >= 8) {
      out.push((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  return Buffer.from(out);
}

/** RFC 6238 : SHA-1, six chiffres, pas de trente secondes. */
export function totp(secret: string, at = Date.now()): string {
  const counter = Math.floor(at / 1000 / 30);
  const message = Buffer.alloc(8);
  message.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
  message.writeUInt32BE(counter % 2 ** 32, 4);

  const digest = createHmac('sha1', decode(secret)).update(message).digest();
  const offset = digest[digest.length - 1] & 0x0f;
  const binary =
    ((digest[offset] & 0x7f) << 24) |
    (digest[offset + 1] << 16) |
    (digest[offset + 2] << 8) |
    digest[offset + 3];

  return String(binary % 1_000_000).padStart(6, '0');
}

Ce que ça change pour une suite de tests

Le parcours complet est testable

Activation de la double authentification, connexion, code de secours : rien ne dépend plus d’un appareil physique ni d’une boîte mail personnelle.

Le test reste lisible

Deux appels et une assertion. Le code n’est pas extrait par vous, et l’attente n’est pas une boucle que vous maintenez.

Les exécutions parallèles ne se marchent pas dessus

Chaque test reçoit à son adresse, donc le code lu est celui de son propre parcours.

Et un greffon Playwright ?

Prévu, pas publié. Le test ci-dessus n’attend rien de lui : il appelle l’API directement, et continuera de fonctionner ensuite.

Voir l’état de chaque intégration

Débloquez vos tests 2FA

Une boîte de test, une clé en lecture seule, et le parcours entier redevient testable. Le plan gratuit ne demande pas de carte bancaire.