OTP- en verificatiemails: snelheid, beveiliging en best practices

Zo genereer, bewaar en mail je eenmalige codes en verificatielinks veilig: snelle bezorging, korte geldigheid, rate limits en een template die gebruikers vertrouwen.

Een e-mail met een eenmalige code is het meest ongeduldige bericht dat je product ooit verstuurt. De gebruiker staart naar een scherm met “Voer de 6-cijferige code in die we je hebben gestuurd”, en elke seconde dat de e-mail onderweg is, is een seconde waarin hij twijfelt of hij het opgeeft.

Verificatiemails liggen bovendien precies op de grens van je beveiliging. Doe je het verkeerd, dan sluit je echte gebruikers buiten of laat je aanvallers binnen. Deze gids behandelt beide kanten: de code snel in de inbox krijgen, en zorgen dat alleen de juiste persoon hem kan gebruiken.

Er zijn twee gangbare vormen:

  • Eenmalige codes (OTP): een kort getal zoals 482913, dat de gebruiker in je app intypt. Geschikt voor inloggen en tweestapsverificatie, en ze werken ook als de gebruiker zijn e-mail op een ander apparaat leest.
  • Verificatielinks: een lang willekeurig token in een URL. Geschikt om een e-mailadres te bevestigen bij aanmelding of om een wachtwoord te resetten, omdat één klik de flow afrondt.

Alles hieronder geldt voor beide. Het verschil zit vooral in de lengte: links kunnen een lang token bevatten, codes moeten kort genoeg zijn om in te typen.

Genereer codes veilig

Gebruik de cryptografisch veilige randomgenerator van je programmeertaal. Math.random() en de module random van Python zijn voorspelbaar en mogen nooit voor beveiligingscodes worden gebruikt.

// Node.js
import { randomInt, createHmac } from 'node:crypto';

const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); // "048213"
// Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
const codeHash = createHmac('sha256', process.env.OTP_SECRET).update(`${user.id}:${code}`).digest('hex');
# Python
import hashlib
import hmac
import os
import secrets

code = f"{secrets.randbelow(1_000_000):06d}"
# Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
code_hash = hmac.new(os.environ["OTP_SECRET"].encode(), f"{user_id}:{code}".encode(), hashlib.sha256).hexdigest()

Sla alleen een hash met sleutel (HMAC) op, samen met het gebruikers-ID, een vervaltijd en een teller voor pogingen. Een gewone SHA-256 is niet genoeg: met maar een miljoen mogelijke codes kan iedereen die de database in handen heeft ze in minder dan een seconde allemaal proberen. De HMAC vereist een secret dat buiten de database wordt bewaard, zodat een databaselek op zich geen geldige codes prijsgeeft.

Laat codes snel verlopen en beperk pogingen

Een zescijferige code heeft een miljoen mogelijkheden. Dat is ruim voldoende, maar alleen als aanvallers niet eindeloos kunnen blijven raden:

Maatregel Aanbevolen waarde
Geldigheid van de code 10 minuten voor inlogcodes; tot 24 uur voor links om een e-mailadres te bevestigen
Foute pogingen per code 5, daarna is de code ongeldig
Verzonden codes per adres Ongeveer 3 per 15 minuten, met een zichtbare wachttijd bij “Opnieuw versturen”
Verzonden codes per IP-adres Een hogere limiet, zodat één aanvaller niet door veel adressen heen kan gaan
Gebruik Eenmalig: verwijder de code of markeer hem als gebruikt zodra hij is geaccepteerd

Vergelijk codes altijd in constante tijd (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python), en zorg dat een nieuwe code de vorige ongeldig maakt.

Verstuur de code meteen

OTP-mails moeten de deur uit in hetzelfde verzoek waarin ze zijn aangevraagd, niet in een batchjob die elke minuut draait. Met Inbixio is dat één aanroep:

await fetch('https://backend.inbixio.com/api/send-email/', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.INBIXIO_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    to_email: user.email,
    subject: `${code} is your YourApp sign-in code`,
    message: otpTemplate({ code, minutes: 10 }),
  }),
  signal: AbortSignal.timeout(10_000),
});

Inbixio accepteert het verzoek en zet de bezorging direct in de wachtrij, zodat je endpoint snel kan antwoorden. Mislukt de aanroep, laat het de gebruiker dan meteen weten (“We konden je code niet versturen. Probeer het zo opnieuw”) in plaats van hem te laten wachten op een e-mail die nooit komt. Fouten van de e-mail-API afhandelen laat zien welke fouten een automatische nieuwe poging waard zijn.

Twee bezorgtips die specifiek voor codes gelden:

  • Gebruik een aparte afzender, zoals security@yourco.com, gekoppeld als eigen server, met een API-sleutel die daarop is vastgezet (zet de sleutel opnieuw vast als je die server ooit verwijdert). Gebruikers leren dat adres te vertrouwen, en marketingcampagnes kunnen de reputatie ervan niet aantasten.
  • Authenticeer je domein. Codes die in de spamfolder belanden, zijn codes die nooit aankomen. Loop de checklist voor SPF, DKIM en DMARC door.

Schrijf een e-mail die niet voor phishing door kan gaan

Je template concurreert met phishingmails die precies dit format nabootsen. Maak de echte makkelijk herkenbaar:

  • Zet de code in de onderwerpregel, bijvoorbeeld “482913 is je inlogcode voor YourApp”. Gebruikers kunnen hem dan in de melding lezen zonder de e-mail te openen, en toetsenborden op telefoons kunnen hem voorstellen.
  • Vermeld waarvoor de code is en hoe lang hij geldig is: “Gebruik deze code om in te loggen bij YourApp. Hij verloopt over 10 minuten.”
  • Vertel wat te doen als de gebruiker het niet zelf was: “Heb je niet geprobeerd in te loggen? Dan kun je deze e-mail negeren. Je account is veilig.”
  • Vraag nooit om de code terug. Zeg het duidelijk: “YourApp zal je nooit telefonisch of per e-mail om deze code vragen.”
  • Houd het kort, zonder marketing, en beperk links tot het absolute minimum.

Een eenvoudige, toegankelijke HTML-template:

<div style="font-family:system-ui,sans-serif;max-width:480px;margin:auto;color:#111">
  <p>Use this code to sign in to YourApp:</p>
  <p style="font-size:32px;font-weight:700;letter-spacing:6px;margin:24px 0">482913</p>
  <p>It expires in 10 minutes. If you didn't try to sign in, you can ignore this email.</p>
  <p style="color:#666;font-size:13px">YourApp will never ask you for this code.</p>
</div>

Inline styles zijn een bewuste keuze. Veel e-mailclients verwijderen <style>-blokken, dus inline CSS is de betrouwbaarste manier om e-mail op te maken.

Verraad niet welke e-mailadressen een account hebben

Toon op een inlogscherm of een scherm “stuur me een code” dezelfde respons, of het adres nu geregistreerd is of niet: “Als er een account bestaat voor dat adres, hebben we een code gestuurd.” Anders wordt je formulier een gratis tool om te controleren welke e-mailadressen je product gebruiken.

Checklist

  • Codes uit een cryptografisch veilige generator; alleen HMAC’s opgeslagen, met het secret buiten de database
  • Geldigheid van 10 minuten, eenmalig gebruik, 5 pogingen, vergelijking in constante tijd
  • Rate limits per adres en per IP, met een wachttijd voor opnieuw versturen
  • Direct verstuurd, met een duidelijke foutmelding als verzenden mislukt
  • Apart, geauthenticeerd afzenderadres
  • Code in het onderwerp, doel en geldigheid in de body, uitleg “was jij dit niet?”
  • Dezelfde respons, of het account nu bestaat of niet

Verstuur het met Inbixio

Koppel je eigen SMTP-server, maak een API-sleutel met beperkte rechten aan en verstuur vanuit elke programmeertaal met één HTTPS-verzoek.

Account aanmaken Bekijk prijzen