Wachtwoordreset en 2FA-code per e-mail versturen
Wachtwoordresets en tweestapsverificatie zijn klassieke transactionele e-mails, en tegelijk de berichten waar beveiliging het meest telt. In deze tutorial laten we de flow zien en de keuzes die je veilig houden.
De flow in het kort
Verzoek
De gebruiker vraagt een reset of logt in met 2FA.
Token
Je genereert een eenmalig token of een code met een cryptografisch veilige generator.
Opslaan
Je slaat een gehashte versie op met een korte vervaltijd.
Mailen
Je stuurt de link of de code naar de gebruiker.
Verbruiken
Bij gebruik controleer je het token en maak je het direct ongeldig.
Wachtwoordreset versturen
import { Wesender } from "@wesender/node"
const ws = new Wesender(process.env.WS_API_KEY!)
export async function stuurResetMail(to: string, resetUrl: string) {
return ws.emails.send({
from: "noreply@joudomein.nl",
to,
subject: "Stel je wachtwoord opnieuw in",
html: `
<p>
Klik op de link hieronder om je wachtwoord te resetten.
<strong>De link verloopt over 30 minuten.</strong>
</p>
<p><a href="${resetUrl}">Wachtwoord resetten</a></p>
<p>Heb jij dit niet aangevraagd? Dan hoef je niets te doen.</p>
`,
})
} 2FA-code versturen
export async function stuur2faCode(to: string, code: string) {
return ws.emails.send({
from: "noreply@joudomein.nl",
to,
subject: "Je inlogcode",
html: `
<p>Je code is <strong style="font-size:1.5em">${code}</strong>.</p>
<p>Hij is <strong>5 minuten</strong> geldig. Gebruik hem maar één keer.</p>
`,
})
} Token genereren en hashen
Gebruik de ingebouwde crypto-module voor een cryptografisch veilig token. Sla nooit het token zelf op, maar een bcrypt-hash ervan:
import { randomBytes } from "crypto"
import { hash, compare } from "bcryptjs"
export async function maakResetToken() {
const token = randomBytes(32).toString("hex") // 64-tekens hex
const tokenHash = await hash(token, 10)
return { token, tokenHash }
}
// Sla de hash op, stuur het token naar de gebruiker
export async function slaResetOp(email: string) {
const { token, tokenHash } = await maakResetToken()
await db.passwordResets.upsert({
where: { email },
create: { email, tokenHash, expiresAt: new Date(Date.now() + 30 * 60_000) },
update: { tokenHash, expiresAt: new Date(Date.now() + 30 * 60_000) },
})
return token // stuur dit naar de gebruiker, bewaar de hash in de DB
}
// Verifieer bij gebruik
export async function verifieerToken(email: string, token: string) {
const reset = await db.passwordResets.findUnique({ where: { email } })
if (!reset || reset.expiresAt < new Date()) return false
const geldig = await compare(token, reset.tokenHash)
if (geldig) await db.passwordResets.delete({ where: { email } }) // eenmalig
return geldig
} Rate limiting
Beperk het aantal resetverzoeken per IP-adres of e-mailadres. Zo voorkom je dat aanvallers je systeem overspoelen of gebruikers lastigvallen met ongewenste mails:
// Eenvoudige in-memory limiter (gebruik Redis in productie voor schaalbaarheid)
const pogingen = new Map<string, { aantal: number; resetAt: number }>()
export function checkRateLimit(sleutel: string, max = 5, vensterMs = 60_000): boolean {
const nu = Date.now()
const entry = pogingen.get(sleutel)
if (!entry || entry.resetAt < nu) {
pogingen.set(sleutel, { aantal: 1, resetAt: nu + vensterMs })
return true
}
if (entry.aantal >= max) return false
entry.aantal++
return true
}
// In je Express route handler:
app.post("/forgot-password", async (req, res) => {
if (!checkRateLimit(`reset:${req.ip}`)) {
return res.status(429).json({ message: "Te veel pogingen. Wacht een minuut." })
}
// Toon altijd dezelfde bevestiging, ook als het adres onbekend is
await slaResetOp(req.body.email).catch(() => {})
res.json({ message: "Als dit adres bij ons bekend is, ontvang je een e-mail." })
}) Beveiligingstips
Bij reset en 2FA zijn dit de keuzes die tellen:
- Laat tokens en codes snel verlopen: een reset binnen 30 minuten, een 2FA-code binnen 5 minuten.
- Maak een token of code eenmalig: na gebruik direct ongeldig maken.
- Sla resettokens gehasht op met een veilig hash-algoritme, nooit als platte tekst.
- Beperk het aantal verzoeken per tijdsperiode zodat misbruik moeilijk wordt.
- Onthul niet of een e-mailadres bestaat. Toon altijd dezelfde bevestiging.
- Zorg dat deze mails daadwerkelijk aankomen. Het zijn juist de berichten waar alles van afhangt.
Vragen
Hoe lang mag een resetlink geldig zijn?
Moet ik onthullen of een e-mailadres bestaat?
Hoe sla ik een resettoken op?
Wat is het verschil tussen TOTP en een e-mailcode voor 2FA?
Moet de resetlink verplicht over HTTPS?
Gerelateerde artikelen
Transactionele e-mail versturen?
3.000 e-mails per maand gratis. Data in Europese datacenters.