E-maile z kodem OTP: szybkość, bezpieczeństwo i dobre praktyki

Jak bezpiecznie generować, przechowywać i wysyłać kody jednorazowe i linki weryfikacyjne: szybka dostawa, rozsądny czas ważności, limity i zaufany szablon.

E-mail z kodem jednorazowym to najbardziej niecierpliwa wiadomość, jaką kiedykolwiek wyśle Twój produkt. Użytkownik patrzy na ekran z napisem „Wpisz 6-cyfrowy kod, który Ci wysłaliśmy”, a każda sekunda oczekiwania na e-mail to sekunda, w której zastanawia się, czy nie zrezygnować.

E-maile weryfikacyjne leżą też dokładnie na granicy Twoich zabezpieczeń. Jeśli zrobisz je źle, albo zablokujesz prawdziwych użytkowników, albo wpuścisz atakujących. Ten poradnik omawia obie strony: jak szybko dostarczyć kod do skrzynki i jak zadbać, by użyć go mogła tylko właściwa osoba.

Kody czy linki?

Są dwie popularne formy:

  • Kody jednorazowe (OTP): krótka liczba, np. 482913, wpisywana z powrotem w aplikacji. Dobre do logowania i weryfikacji dwuetapowej; działają też, gdy użytkownik czyta e-maile na innym urządzeniu.
  • Linki weryfikacyjne: długi losowy token w adresie URL. Dobre do potwierdzania adresu e-mail przy rejestracji albo resetowania hasła, bo jedno kliknięcie kończy cały proces.

Wszystko poniżej dotyczy obu form. Różnią się głównie długością: link może zawierać długi token, a kod musi być na tyle krótki, by dało się go przepisać.

Bezpieczne generowanie kodów

Używaj kryptograficznie bezpiecznego generatora liczb losowych w swoim języku. Math.random() i moduł random w Pythonie są przewidywalne i nigdy nie wolno ich używać do kodów bezpieczeństwa.

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

Przechowuj wyłącznie skrót z kluczem (HMAC) razem z identyfikatorem użytkownika, czasem wygaśnięcia i licznikiem prób. Zwykły SHA-256 nie wystarczy: przy zaledwie milionie możliwych kodów każdy, kto ma dostęp do bazy danych, sprawdzi je wszystkie w niecałą sekundę. HMAC wymaga sekretu przechowywanego poza bazą danych, więc sam wyciek bazy nie ujawnia aktywnych kodów.

Krótka ważność, limit prób

Sześciocyfrowy kod ma milion możliwości. To w zupełności wystarczy, ale tylko wtedy, gdy atakujący nie mogą zgadywać bez końca:

Zabezpieczenie Zalecana wartość
Czas ważności kodu 10 minut dla kodów logowania; do 24 godzin dla linków potwierdzających adres e-mail
Błędne próby na kod 5, potem kod traci ważność
Kody wysłane na jeden adres Około 3 na 15 minut, z widocznym odliczaniem przy „Wyślij ponownie”
Kody wysłane z jednego adresu IP Wyższy limit, który powstrzyma jednego atakującego przed przechodzeniem przez wiele adresów
Użycie Jednorazowe: usuń kod lub oznacz go jako użyty w chwili udanej weryfikacji

Zawsze porównuj kody w stałym czasie (crypto.timingSafeEqual w Node.js, hmac.compare_digest w Pythonie), a wydanie nowego kodu powinno unieważniać poprzedni.

Wysyłaj natychmiast

E-maile z kodem OTP powinny wychodzić w tym samym żądaniu, które o nie poprosiło, a nie w zadaniu wsadowym uruchamianym co minutę. W Inbixio to jedno wywołanie:

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 przyjmuje żądanie i od razu kolejkuje dostarczenie, więc Twój endpoint może szybko odpowiedzieć. Jeśli wywołanie się nie powiedzie, od razu poinformuj użytkownika („Nie udało się wysłać kodu. Spróbuj ponownie za chwilę”), zamiast zostawiać go w oczekiwaniu na e-mail, który nigdy nie dotrze. Artykuł obsługa błędów API e-mail wymienia błędy, które warto automatycznie ponawiać.

Dwie wskazówki dotyczące dostarczania kodów:

  • Używaj osobnego nadawcy, np. security@yourco.com, podłączonego jako osobny serwer, z kluczem API przypiętym do niego (przypnij klucz ponownie, jeśli kiedykolwiek usuniesz ten serwer). Użytkownicy uczą się ufać temu adresowi, a kampanie marketingowe nie mogą wpłynąć na jego reputację.
  • Uwierzytelnij swoją domenę. Kody, które trafiają do spamu, to kody, które nigdy nie docierają. Przejdź przez checklistę SPF, DKIM i DMARC.

E-mail, którego nie da się pomylić z phishingiem

Twój szablon konkuruje z e-mailami phishingowymi, które kopiują dokładnie ten format. Spraw, by prawdziwy łatwo było rozpoznać:

  • Umieść kod w temacie, np. „482913 to Twój kod logowania do YourApp”. Użytkownik odczyta go z powiadomienia bez otwierania e-maila, a klawiatura w telefonie może go podpowiedzieć.
  • Napisz, do czego służy i jak długo jest ważny: „Użyj tego kodu, aby zalogować się do YourApp. Wygaśnie za 10 minut”.
  • Napisz, co zrobić, jeśli to nie on: „Jeśli nie próbowałeś się zalogować, zignoruj tę wiadomość. Twoje konto jest bezpieczne”.
  • Nigdy nie proś o odesłanie kodu. Napisz to wprost: „YourApp nigdy nie poprosi Cię o ten kod telefonicznie ani e-mailem”.
  • Pisz krótko, bez marketingu, i ogranicz linki do absolutnego minimum.

Prosty, dostępny szablon HTML:

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

Style inline są celowe. Wiele klientów pocztowych usuwa bloki <style>, więc CSS inline to najpewniejszy sposób formatowania e-maili.

Nie zdradzaj, które adresy mają konta

Na ekranie logowania lub „wyślij mi kod” pokazuj tę samą odpowiedź niezależnie od tego, czy adres jest zarejestrowany: „Jeśli dla tego adresu istnieje konto, wysłaliśmy kod”. W przeciwnym razie Twój formularz staje się darmowym narzędziem do sprawdzania, które adresy e-mail korzystają z Twojego produktu.

Checklista

  • Kody z kryptograficznie bezpiecznego generatora; przechowywane tylko HMAC-i, z sekretem poza bazą danych
  • Ważność 10 minut, jednorazowe użycie, 5 prób, porównanie w stałym czasie
  • Limity na adres i na IP, z odliczaniem przed ponowną wysyłką
  • Wysyłka natychmiastowa, z jasnym komunikatem błędu, jeśli się nie powiedzie
  • Osobny, uwierzytelniony adres nadawcy
  • Kod w temacie, cel i czas ważności w treści, wskazówka „to nie Ty?”
  • Ta sama odpowiedź niezależnie od tego, czy konto istnieje

Wyślij to przez Inbixio

Podłącz własny serwer SMTP, utwórz klucz API z ograniczonymi uprawnieniami i wysyłaj z dowolnego języka jednym żądaniem HTTPS.

Załóż konto Zobacz cennik