Галоўная / Артыкулы / Блакаванне электронных паштакоў з просьбай аб рэгістрацыі, якія не можаць быць даставлены, за дапамою Supabase Auth Hook і DNS

Блакаванне электронных паштакоў з просьбай аб рэгістрацыі, якія не можаць быць даставлены, за дапамою Supabase Auth Hook і DNS

Ўзайце хук Supabase before-user-created і запыткі MX, A і AAAA, каб адхоўваць рэўізію рэгістрацый для тых, чыя домены электронной пашты не можаць прымоўваць лісты, сэргаваючыся правіламі RFC 5321.

1680 слоў

Форма рэгістрацыі, якая пераглядае электронныя адрысы толькі за дапамою звычайнага выразу, прыме ўсё, што нагадвае адрыс, укладзены ў домэнах, якія вообща не можу прымаць пашту. Пакалькі Supabase стварае запис прыўязанага корыстніка ўжо перад тым, як будзе нажата кнопка па пאўтарнай пераправцы пашты, кожны такі адрыс заставляе ў вашай базе дадзеных стварыцца неперакананы рядок. У гэтым кяліку показана, як пераспецыяваць процес рэгістрацыі за дапамою хука Supabase Auth і як выкарыстоўваць запыты DNS, каб адхіліць домэны, у якіх няма працюючага сервера пашты, што гаворяць адпаведныя правілы SMTP, і дзе неабходна падсиліць захаванне першы чым працэс будзе адкрыты для шырокай аудытарыі.

Дзе падключыцца да процесу рэгістрацыі

За дапамою Supabase запыткі на рэгістрацыю зазвычай надходзяць безпосередна з кліентскай бібліятэкі, таму няма вашага сэрверскага маршрута, дзе можна было б спачатку пераканаліць адрес. Supabase рашыяе гэтыя проблемы за дапамою Auth Hooks: канцэнтры або функцый базы дадзеных, якія Supabase Auth вызывае у конкрэтныя моменты своей роботы. Хук before-user-created выконваецца перад дадзеннем новага корыстніка і можа празраціць або адхіліць запытку.

Фрагменты коду нижэй прынесены з проекта Next.js, але сама пераканаліць выконваецца ў функцыі Supabase Edge Function, таму яна не залежыць ад вашай фронтэнд-фрамворкі.

Актывацыя хука ў локальным проекте

Калі Supabase запускаецца локальная форма через CLI, інтэрфейс Studio можа не мець настаўкі для Auth hooks. У такім случыку актывацыйте гак у supabase/config.toml, знімаючы коментары і заповніўшы гэты раздзял:

#[auth.hook.before_user_created]
#enabled = true
#secrets="v1,whsec_ some secret" (needs to be added)
#uri = "http://host.docker.internal:54321/functions/v1/before-user-created"

uri — это канцэнтрык, які будзе вызваны Supabase, а secrets мае таямніцу падпісаў, якую Supabase выкарыстоўвае для падпісаў кожнага запиту гака. Зверніце увагу на хост: выкарыстоўваецца host.docker.internal, таму што локальны стак Supabase запускаецца ў Docker, і гэтае імя дазволяе його контейнерам прабівацца да сервера функцый.

Стварэнне Edge Function

Канцэнтрык тут — это Edge Function, функцыя з боку сервера, якую Supabase размешчае ў глобальна розпадзеленым сервісе выканання, які знаходзіцца поблізу корыстувачаў. Створыце яе за дапамойкай CLI:

npx supabase functions new before-user-created

Каманда стварае новы папаку пад supabase/functions, а логіка знаходзится ў файле index.ts. За інструкцыямі Supabase да хука before-user-created, Supabase адправляе пакет з метаданымі запиту і інформацыяй пра пользователя, які зараз будзе створаны:

{
 "metadata": {
 "uuid": "8b34dcdd-9df1–4c10–850a-b3277c653040",
 "time": "2025–04–29T13:13:24.755552–07:00",
 "name": "before-user-created",
 "ip_address": "127.0.0.1"
 },
 "user": {
 "id": "ff7fc9ae-3b1b-4642–9241–64adb9848a03",
 "aud": "authenticated",
 "role": "",
 "email": "valid.email@supabase.com",
 "phone": "",
 "app_metadata": {
 "provider": "email",
 "providers": ["email"]
 },
 "user_metadata": {},
 "identities": [],
 "created_at": "0001–01–01T00:00:00Z",
 "updated_at": "0001–01–01T00:00:00Z",
 "is_anonymous": false
 }
}

Поле, яке мае значэнне для гэтай пераканалки, — user.email.

До якога момента трэба вяршыць пераканалку?

Калі у вас ёсць адреса, елемент домэны можна пераканаліць за дапамою DNS. Можна пайсці даўжэй: вычысліць IP-адресу поштовага сервера, стварыць TCP-з’ўязак з яй і пераканаліць роботу протакола SMTP. Але гэта зазвычай не мае сенсу. Багаты серверы адмовляюць у такіх пераканалках або вярцаюць, гэтыя пераканалкі дадаюць значны затрымкі практычна ў кожнай процэсе регистрацыі, а едынам надзейным доказам існавання поштовага ящыка є тое, калі пользователя нажме на лінк па падтверджэнне.

Прагматычны ўзьязд — пераканацца, што домэн мае прынеймна адны сервер пашты, які насправды ператвараецца ў адресу IP. Гэта дазволяе недорага адфільтраваць памылкі написання і вымышленыя домэны. Людзі, якія жадаюць заблокаваць вашу базу даных, все равно можаць зарэўнаваць рэальныя домэны, таму спрыймайце гэта як фільтр проты шуму, а не як захопленне ад зловжыванняў. Ступень дадатковых заходаў залежыць ад вашага модэлю апасцярожнасці.

Што гаворяць правілы SMTP пра записы MX

Домэны публікуюць своія серверы пашты як записы MX (mail exchange), як гэта вяліка для SMTP у RFC 5321. Модуль dns у Node адкрывае можлівасць викорыстоўваць resolveMx(), який вяртае хост exchange і priority кожнага запису, а таксама resolve4() і resolve6(), якія вяртаюць адресы IPv4 і IPv6 хоста. Тры правілы вяліка, чы можа домэн прымаць пашту:

  1. Адзінаковасць MX-запісей значыць імплікатывны MX. У раздзеле RFC пра выявленне цэльвог хоста зазначаецца, што калі спіс MX пусты, сам домэн вважаецца серверам пашты. Такім чынам, саме A- або AAAA-запісы домэна вяліканае рашынку.
  2. Якшто всі MX-запісы неякісны, гэта ўскладненне. У там самым раздзеле выклекванае, што якшто MX-запісы існуюць, але жадна з іх не працюе, доставка пашты павінна зазнаць нявяскоў. У практыцы: якшо жадны з указаных сервераў адміністрацыі не ператвараецца на IP-адрэс, запрос на регистрацыю трэба адхіліць.
  • Нябытна MX-значэнне означае, што домен не прымае пашту. RFC 7505 (раздзел 3) апішае адно MX-запісаў з прэферанцыяй 0 і порожнім адресам для адменавання, які пісаецца як "." у файлах зоны (формат запісаў описаны ў раздзеле 3.3.9 RFC 1035), як чыстае пазначэнне таго, што домен не прымае электранявісныя пашты.
  • Адкалканне хука

    Наведаны выкладка прыменяе тыя правілы. Яна чытае пэйлоад, выделяе домен пасля @ і вяртае 400 у разе некоректнага вхідных дадзенняў. Пасля чыго яна шукае MX-запісы і обрабоцвае кожны случай: якщо запісаў няма, яна пераглядае сабственныя адресы домена; якщо є адзін недзейны MX-запіс, яна адхоўляе запыт; інакш яна праходзіць па всіх запісах і прыймае домен, як толькі хоць аднам з іх будзе выдаўцы IP-адресы. Пустая об’ект JSON сігналізуе Supabase пра продаж чынення. Памочны функцыя hasIpAddress адбывае пошук IPv4 і IPv6 паралельна за дапамою Promise.allSettled, тады якшо адна з пацікаў не успее, гэта не будзе маскаваць успех у іншай.

    import "@supabase/functions-js/edge-runtime.d.ts";
    import { withSupabase } from "@supabase/server";
    import dns from "node:dns/promises";
    
    export default {
      fetch: withSupabase({ auth: "none" }, async (req) => {
        // Called by another service with a secret key
        // ctx.supabaseAdmin bypasses RLS — use for privileged operations
    
        try {
          const r = await req.json();
          const email = r.user.email;
    
          //seems like strict email validation is not required,initial thoughts,
          //  needs to be investigated
    
          if (typeof email !== "string") {
            return Response.json({
              error: {
                message: "",
              },
            }, {
              status: 400,
            });
          }
    
          const domain = (email as string).split("@")[1];
    
          if (!domain) {
            return Response.json({
              error: {
                message: "bad request",
              },
            }, {
              status: 400,
            });
          }
    
          const mx_records = await dns.resolveMx(domain);
    
          if (mx_records.length === 0) { //case 1: no mx record present
            // email server may be domain itself
            const mail_server = domain;
            const has_ip_address = await hasIpAddress(mail_server);
            if (has_ip_address) {
              return Response.json({});
            } else {
              return Response.json({
                error: {
                  message: "bad request",
                },
              }, {
                status: 400,
              });
            }
          } else {
            if (mx_records.length === 1) { // case 3: domain does not accept mails
              const record = mx_records[0];
              if (record.priority === 0 && record.exchange === ".") {
                return Response.json({
                  error: {
                    message: "bad request",
                  },
                }, {
                  status: 400,
                });
              }
            }
    
            for (const record of mx_records) { // inspection for case 2
              if (record.exchange) {
                const has_ip_address = await hasIpAddress(record.exchange);
                if (has_ip_address) {
                  return Response.json({});
                }
              }
            }
    
            return Response.json({
              error: {
                message: "bad request",
              },
            }, {
              status: 400,
            });
          }
        } catch (_e) {
          return Response.json({
            error: {
              message: "internal server error",
            },
          }, {
            status: 500,
          });
        }
      }),
    };
    
    const hasIpAddress = async (mail_server: string): Promise<boolean> => {
      const [ipv4, ipv6] = await Promise.allSettled([
        dns.resolve4(mail_server),
        dns.resolve6(mail_server),
      ]);
    
      return (ipv4.status === "fulfilled" && ipv4.value.length > 0) ||
        (ipv6.status === "fulfilled" && ipv6.value.length > 0);
    };
    

    Проблемы ў гэтым коде, якія варта выправіць

    Логіка адпавядае стандартам RFC, але калькі деталей працююць інакш, чым прымае код:

    • resolveMx() выклікае адзінакоўнае правіла, замест таго каб вярнуць порожняй список. Для домэны без записаў MX або для неіснуючай домэны Node адхронівае з такімі памылкамі, як ENODATA або ENOTFOUND. Гэта значыць, што ветка з імпліцытным MX рэдка калі запускаецца; у замест на гэта блок catch вярнае код 500, і законныя домэны, якія выкарыстоўваюць імпліцытны MX, адхроняюцца як памылкі сервера. Прыйміце памылку ENODATA ў апэрабоце resolveMx() і перайдзіце на варыянт hasIpAddress(domain).
    • Перакананне на наявнасць null MX можа не быць точным. Node адзвяртае увагу на null MX за дапамою порожней строчкі exchange, а не за дапамою симвала ".". Умова if (record.exchange) у цыкле все равно адхронівае такія домэны, але краща порыхаваць оба значэння, ўбачліваючы намер.
  • Падэж падписі ніколі не пераканальвается. У адгукаванні памятаецца пра секрэт, але auth: "none" прымае будзь-каго вызывача. Пераканальвайце падпісаны запит, выкарыстоўваючы секрэт, настроены ў config.toml, як описана ў дакументацыі к падключэннях Auth, каб стороннія ўчаснікі не моглі вызваць функцыю.
  • Пераканальвайце формат адхілення. Supabase чакае бягучыя памылкі у спецыяльным формате (заўключаючы код стану HTTP у об’екте памылкі); пераканальвайце текущы формат у дакументацыі падключэння, каб корыстувальнікі бачылі зместовую паведамленне.
  • Дадзіце таймауты. Медленныя серверы DNS безпосередна затрымляюць процес рэўістрацыі, таму задаць таймаут для кожнага пошуку.
  • Час выканання: Функцыі Edge запускаюцца на Deno, а node:dns/promises працюе чераз яго шар сумаспадобнасці з Node. Прабавайце яго ў месцах размешчэння вашага продакту.
  • Дазволенне непераканальваных вызываў да функцыі

    На завершанне зарэгіструйце функцыю ў config.toml і выключыце перакананне JWT для яе. Назва ў квадратных скобках павінна адпавядаць назве папкі з функцыяй у supabase/functions:

    [functions.before-user-created]
    verify_jwt = false
    

    Перакананне JWT выключана, таму што калі відбываецца рэўізія, ніхто ўсё яшчэ не заўважаны; сама пераканання падпісу хука, а не токен пользователя, які должен захістваць гэты канец.

    Ключовыя моменты

    • Хук Auth before-user-created дазволяе пераканаць адресы на стороне сервера нават тады, калі рэўізія запускаецца з боку кліентскай бібліятэкі.
    • Домен можа прымоць пісьма, якщо аднаколькі з його MX-хостаў рэшуецца, або, якщо няма MX-запісаў, якшчо сам домен рэшуецца; ноль у MX означае, што ён не прымае нічога.
    • У Node функцыя resolveMx() выклекае адсутнасць запісаў, таму трэба явна обрабоцваць ENODATA, а не спадзявацца на порожній масэвы.
  • DNS-перагляды выявляюць памылкі написання і фальшывыя домены, а не рэальныя, але невыкарыстоўваныя поштовыя скринькі; палявка на электранаўпыт застаецца ўжо ўсёй прабамой прыналежнасці.
  • Пераканайцеся ў падпісе хука і часах адыскання пры выкарыстоўванні яго ў рэальных умовах, прытым як прабуючы на ўсёй продукцыі.