OTP- und Verifizierungs-E-Mails: Geschwindigkeit, Sicherheit und Best Practices

Einmalcodes und Bestätigungslinks sicher erzeugen, speichern und per E-Mail senden: schnelle Zustellung, sinnvolle Ablaufzeiten, Rate-Limits und eine seriöse Vorlage.

Eine E-Mail mit Einmalcode ist die ungeduldigste Nachricht, die Ihr Produkt je versenden wird. Der Nutzer starrt auf einen Bildschirm mit der Aufforderung „Geben Sie den 6-stelligen Code ein, den wir Ihnen gesendet haben“, und jede Sekunde, die die E-Mail braucht, ist eine Sekunde, in der er überlegt, ob er aufgibt.

Verifizierungs-E-Mails liegen außerdem direkt an Ihrer Sicherheitsgrenze. Machen Sie hier Fehler, sperren Sie entweder echte Nutzer aus oder lassen Angreifer herein. Dieser Leitfaden behandelt beide Seiten: den Code schnell in den Posteingang zu bringen und sicherzustellen, dass nur die richtige Person ihn verwenden kann.

Es gibt zwei gängige Formen:

  • Einmalcodes (OTP): eine kurze Zahl wie 482913, die in Ihre App eingetippt wird. Gut für die Anmeldung und die Zwei-Faktor-Verifizierung, und sie funktionieren auch, wenn der Nutzer E-Mails auf einem anderen Gerät liest.
  • Bestätigungslinks: ein langer, zufälliger Token in einer URL. Gut, um bei der Registrierung eine E-Mail-Adresse zu bestätigen oder ein Passwort zurückzusetzen, denn ein Klick schließt den Vorgang ab.

Alles Folgende gilt für beide. Der Unterschied liegt vor allem in der Länge: Links können einen langen Token tragen, Codes müssen kurz genug zum Abtippen sein.

Codes sicher erzeugen

Verwenden Sie den kryptografisch sicheren Zufallsgenerator Ihrer Programmiersprache. Math.random() und das Python-Modul random sind vorhersagbar und dürfen nie für Sicherheitscodes verwendet werden.

// 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()

Speichern Sie nur einen schlüsselbasierten Hash (HMAC), zusammen mit der Nutzer-ID, einer Ablaufzeit und einem Zähler für Versuche. Ein einfacher SHA-256 reicht nicht: Bei nur einer Million möglicher Codes könnte jeder, der die Datenbank besitzt, alle in weniger als einer Sekunde durchprobieren. Der HMAC benötigt ein Secret, das außerhalb der Datenbank liegt, sodass ein Datenbank-Leak allein keine gültigen Codes preisgibt.

Schnell ablaufen lassen, Versuche begrenzen

Ein sechsstelliger Code hat eine Million Möglichkeiten. Das ist nur dann ausreichend, wenn Angreifer nicht unbegrenzt raten können:

Maßnahme Empfohlener Wert
Gültigkeitsdauer des Codes 10 Minuten für Anmeldecodes; bis zu 24 Stunden für Links zur E-Mail-Bestätigung
Fehlversuche pro Code 5, danach ist der Code ungültig
Gesendete Codes pro Adresse Etwa 3 pro 15 Minuten, mit sichtbarer Wartezeit bei „Erneut senden“
Gesendete Codes pro IP-Adresse Eine höhere Obergrenze, damit ein Angreifer nicht viele Adressen durchprobieren kann
Verwendung Einmalig: Löschen Sie den Code oder markieren Sie ihn als verwendet, sobald er erfolgreich eingesetzt wurde

Vergleichen Sie Codes immer in konstanter Zeit (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python), und ein neu ausgestellter Code sollte den vorherigen ungültig machen.

Sofort senden

OTP-E-Mails sollten in derselben Anfrage verschickt werden, die sie angefordert hat – nicht in einem Batch-Job, der jede Minute läuft. Mit Inbixio ist das ein einziger Aufruf:

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 nimmt die Anfrage an und reiht die Zustellung sofort ein, sodass Ihr Endpunkt schnell antworten kann. Schlägt der Aufruf fehl, sagen Sie es dem Nutzer sofort („Wir konnten Ihren Code nicht senden. Bitte versuchen Sie es gleich noch einmal.“), statt ihn auf eine E-Mail warten zu lassen, die nie ankommt. Unter Fehler der E-Mail-API behandeln sehen Sie, welche Fehler einen automatischen Retry wert sind.

Zwei Zustelltipps speziell für Codes:

  • Verwenden Sie einen eigenen Absender, etwa security@yourco.com, als eigenen Server verbunden und mit einem daran gebundenen API-Schlüssel (binden Sie den Schlüssel neu, falls Sie diesen Server je entfernen). Nutzer lernen, dieser Adresse zu vertrauen, und Marketingkampagnen können ihre Reputation nicht beeinträchtigen.
  • Authentifizieren Sie Ihre Domain. Codes, die im Spam landen, sind Codes, die nie ankommen. Arbeiten Sie die Checkliste zu SPF, DKIM und DMARC durch.

Eine E-Mail, die nicht wie Phishing aussieht

Ihre Vorlage konkurriert mit Phishing-E-Mails, die genau dieses Format kopieren. Machen Sie die echte leicht erkennbar:

  • Setzen Sie den Code in die Betreffzeile, z. B. „482913 is your YourApp sign-in code“. Nutzer können ihn in der Benachrichtigung lesen, ohne die E-Mail zu öffnen, und Smartphone-Tastaturen können ihn vorschlagen.
  • Sagen Sie, wofür er ist und wie lange er gilt: „Verwenden Sie diesen Code, um sich bei YourApp anzumelden. Er läuft in 10 Minuten ab.“
  • Sagen Sie, was zu tun ist, wenn es nicht der Nutzer war: „Wenn Sie nicht versucht haben, sich anzumelden, können Sie diese E-Mail ignorieren. Ihr Konto ist sicher.“
  • Fragen Sie den Code nie zurück ab. Schreiben Sie es klar hin: „YourApp wird Sie niemals per Telefon oder E-Mail nach diesem Code fragen.“
  • Halten Sie sie kurz, ohne Marketing, und beschränken Sie Links auf das absolute Minimum.

Eine einfache, barrierearme HTML-Vorlage:

<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>

Die Inline-Styles sind Absicht. Viele E-Mail-Clients entfernen <style>-Blöcke, daher ist Inline-CSS der zuverlässigste Weg, E-Mails zu formatieren.

Nicht verraten, welche Adressen ein Konto haben

Zeigen Sie auf einer Anmelde- oder „Code senden“-Seite dieselbe Antwort, egal ob die Adresse registriert ist oder nicht: „Falls für diese Adresse ein Konto existiert, haben wir einen Code gesendet.“ Andernfalls wird Ihr Formular zu einem kostenlosen Werkzeug, um zu prüfen, welche E-Mail-Adressen Ihr Produkt nutzen.

Checkliste

  • Codes aus einem kryptografisch sicheren Generator; nur HMACs gespeichert, mit dem Secret außerhalb der Datenbank
  • 10 Minuten Gültigkeit, einmalige Verwendung, 5 Versuche, Vergleich in konstanter Zeit
  • Rate-Limits pro Adresse und pro IP, mit Wartezeit für erneutes Senden
  • Sofort gesendet, mit klarer Fehlermeldung, falls das Senden fehlschlägt
  • Eigene, authentifizierte Absenderadresse
  • Code im Betreff, Zweck und Ablaufzeit im Text, Hinweis „Waren Sie das nicht?“
  • Dieselbe Antwort, ob das Konto existiert oder nicht

Mit Inbixio versenden

Verbinden Sie Ihren eigenen SMTP-Server, erstellen Sie einen API-Schlüssel mit passenden Berechtigungen und senden Sie aus jeder Programmiersprache mit einer einzigen HTTPS-Anfrage.

Konto erstellen Preise ansehen