remark42: OAuth przez GitHub i powiadomienia mailowe przez transakcyjny SMTP
Spis treści
Wstęp#
W Self-hosting remark42 obok istniejącego Caddy na VPS postawiłem instancję remark42 z minimalną konfiguracją. Kontener stoi, widget się osadza na blogu, komentować mogą wszyscy jako anonim. Setup startowy jest funkcjonalny do pokazania, że coś działa, ale ma dwa braki, które wychodzą pierwszego dnia realnego użycia: brak sensownego uwierzytelniania i brak jakichkolwiek powiadomień, przez co nowy komentarz można zauważyć tylko przez ręczne odwiedzanie wpisów.
W tym wpisie doprowadzam do działania trzy elementy remark42. Uwierzytelnianie przez GitHub OAuth jako główny kanał logowania, powiadomienia mailowe do admina i do subskrybentów wątków, oraz magic-link email jako trzeci kanał dla osób bez konta GitHub. Prowadzę przez konfigurację konceptualnie, przykłady konkretnych providerów (GitHub jako OAuth, EmailLabs jako transakcyjny SMTP) traktuję jako ilustrację mechaniki, a nie „jedyny słuszny wybór". Jeżeli macie inne preferencje, mapa drogowa zostaje ta sama, zmieniają się tylko wartości w .env.
Uwierzytelnianie przez GitHub OAuth#
OAuth to delegacja weryfikacji tożsamości. remark42 nie trzyma haseł czytelników, tylko pyta providera, czy dana osoba to naprawdę ten, za kogo się podaje, a provider odpowiada podpisanym tokenem. Sam remark42 widzi wynik: nazwę użytkownika, awatar, identyfikator w skali providera. Nic więcej mu nie potrzeba, żeby wystawić własną sesję i powiązać z nią komentarze.
Jako przykład wybieram GitHub. Konwencja AUTH_<PROVIDER>_CID i AUTH_<PROVIDER>_CSEC przenosi się bez zmian na innych providerów wspieranych przez remark42 (Google, GitLab, Microsoft, Facebook i kilku innych), więc jeżeli wybraliście inny, poniższe kroki są takie same, zmieniacie tylko nazwę providera w zmiennych i wchodzicie w jego panel developerski zamiast GitHuba.
Rejestracja OAuth App u providera#
Przed tknięciem .env trzeba zarejestrować aplikację po stronie GitHuba, żeby ten wiedział, komu wolno prosić o zalogowanie użytkownika i gdzie odesłać potwierdzenie. Bez tego kroku GitHub nie zgodzi się na przekierowanie z logowania. Ścieżka w panelu prowadzi przez Settings → Developer settings → OAuth Apps → New OAuth App. Uwaga na nazewnictwo: obok OAuth Apps jest sekcja GitHub Apps, ale to inna rzecz służąca do integracji na poziomie repozytoriów, nie do logowania końcowego użytkownika.
Formularz ma trzy pola wymagane i jeden checkbox. Application name jest widoczne dla użytkownika przy pierwszym logowaniu na ekranie „X wants to access your GitHub account", więc warto, żeby brzmiało jak Wasz blog, nie generyczne „Comments". Homepage URL to adres bloga. Enable Device Flow zostawcie wyłączony, remark42 go nie używa.
Pole, które zjada najwięcej czasu przy debugowaniu, to Authorization callback URL. Musi mieć dokładnie format https://<REMARK_URL>/auth/github/callback, gdzie <REMARK_URL> to publiczna domena Waszej instancji remark42. Ścieżka /auth/github/callback jest sztywna, GitHub porównuje ją bit po bicie z URL-em, który remark42 wysyła w żądaniu OAuth. Każde odstępstwo (http zamiast https, brak /callback, literówka w subdomenie, ukośnik na końcu) kończy się błędem redirect_uri_mismatch i logowaniem, które się nie kończy. To dosłownie najczęstsza pomyłka setupu.
Po zapisaniu formularza GitHub pokazuje Client ID (jawny identyfikator typu Iv1.abcdef123) i pustą sekcję Client secrets. Sekret generujecie przyciskiem Generate a new client secret. Pokazuje się on jeden raz w formie długiego ciągu. Zamknięcie zakładki bez skopiowania oznacza, że musicie wygenerować kolejny. To nie hasło do bazy, tylko uprawnienie do przedstawiania się GitHubowi jako Wasza aplikacja, więc traktujcie je jak każdy inny sekret: trzymajcie w menedżerze haseł, poza gitem, poza logami.
Wpięcie w .env i weryfikacja#
Konfiguracja po stronie remark42 to dwie zmienne:
AUTH_GITHUB_CID=Iv1.abcdef123
AUTH_GITHUB_CSEC=<sekret_z_panelu_GitHuba>
remark42 wykrywa obecność pary CID/CSEC przy starcie i sam dodaje przycisk GitHub do widgetu logowania. Nie trzeba nic klikać po stronie panelu admina, nie ma osobnej sekcji „providers", cała detekcja dzieje się deklaratywnie na podstawie zmiennych środowiskowych. Każda kolejna para (GitLab, Google i tak dalej) dokłada w widgecie kolejny przycisk logowania, wszystkie działają obok siebie.
Po restarcie (docker compose up -d remark42) sprawdźcie logi:
docker compose logs remark42 | grep -i "github\|oauth"
Szukajcie linii mówiącej, że provider GitHub został podniesiony. Otwórzcie widget komentarzy w drugiej przeglądarce (albo trybie prywatnym), kliknijcie „Sign in", powinniście zobaczyć przycisk GitHub obok anonima. Po kliknięciu GitHub pokaże ekran zgody „Authorize X". Akceptacja przeniesie Was z powrotem na bloga jako zalogowanego użytkownika, z awatarem i nickiem z GitHuba.
Multi-tenant przestroga#
Warto zauważyć jeden szczegół, zanim ktoś podejmie decyzję o hostowaniu remark42 dla wielu domen na jednej instancji. OAuth App ma jedną nazwę i jeden callback URL, callback URL wskazuje na instancję remark42, a nie na indywidualną stronę. Jeżeli do jednej instancji podpięliście blog-a.pl i blog-b.pl, czytelnicy obu stron zobaczą na ekranie zgody GitHuba tę samą nazwę aplikacji, niezależnie od tego, z której strony kliknęli „zaloguj". Dla nietechnicznego czytelnika bloga B może to wyglądać jak sygnał ostrzegawczy typu phishing.
To jeden z kilku powodów przemawiających za modelem „jedna instancja remark42 per strona, na tej samej domenie co strona" zamiast centralnego huba. Cała ta dyskusja zasługuje jednak na osobny wpis, tutaj traktujcie ją tylko jako sygnał: jeżeli myślicie o multi-tenancie, sprawdźcie temat głębiej, zanim zbudujecie pod niego infrastrukturę.
Powiadomienia mailowe: dwie osobne rzeczy#
remark42 rozdziela powiadomienia na dwa niezależne kanały. NOTIFY_ADMINS=email wysyła maila do admina o każdym nowym komentarzu, żebyście wiedzieli, że pod jakimś wpisem coś się dzieje. NOTIFY_USERS=email pozwala czytelnikowi zasubskrybować konkretny wątek i dostać maila, kiedy ktoś odpowie na jego komentarz. Oba kanały korzystają z tego samego bloku SMTP_*, więc jeżeli macie skonfigurowaną wysyłkę mailową na potrzeby jednego, drugi macie właściwie za darmo.
Konfiguracja bloku NOTIFY#
Docelowy blok w .env wygląda tak:
NOTIFY_USERS=email
NOTIFY_ADMINS=email
NOTIFY_EMAIL_FROM=comments@example.com
NOTIFY_EMAIL_VERIFICATION_SUBJ=Potwierdź subskrypcję komentarzy
ADMIN_SHARED_EMAIL=twoj@osobisty.email
NOTIFY_EMAIL_FROM to adres widoczny w polu From: wysyłanych maili. Jego domena musi być autoryzowana u providera SMTP, część przed @ nie musi być realną skrzynką. Wartość no-reply@ zadziała technicznie, ale w sekcji o decyzjach produktowych pokażę, dlaczego to gorszy wybór niż komentarze@.
ADMIN_SHARED_EMAIL to Wasz osobisty adres, na który mają lecieć powiadomienia o nowych komentarzach. Ten adres jest niezależny od NOTIFY_EMAIL_FROM, jedno jest nadawcą, drugie odbiorcą. Bez ADMIN_SHARED_EMAIL remark42 nie wie, do kogo wysyłać admin-notification i nie wysyła nic, mimo że NOTIFY_ADMINS=email jest ustawione.
NOTIFY_USERS=email włącza możliwość subskrypcji dla użytkownika, ale nie zapisuje nikogo automatycznie. Bez świadomego opt-inu żaden mail do czytelnika nie poleci.
Subskrypcja wymaga potwierdzenia#
Widget pokazuje pod wątkiem opcję „Subscribe by Email". Czytelnik podaje adres, dostaje mail z tokenem, wkleja go w widget i zaczyna dostawać maile o odpowiedziach w tym wątku.
To nie jest opcjonalne, RODO i CAN-SPAM wymagają potwierdzenia mailem. Bez potwierdzenia mailem ktokolwiek mógłby zapisać cudzy adres na powiadomienia z dowolnego bloga używającego remark42.
Konfiguracja SMTP#
Transakcyjny SMTP to serwis do wysyłania maili z aplikacji zamiast z klienta pocztowego. Providerzy tacy jak EmailLabs, Postmark, Mailgun czy Amazon SES pozwalają skonfigurować własną domenę nadawczą i mają deliverability zbudowaną pod tego typu ruch. Na produkcyjny blog wybieracie dedykowany serwis, nie własnego Gmaila, który wymusza From: na swojej domenie i ma limity dzienne.
Autoryzacja domeny nadawczej odbywa się zgodnie z dokumentacją Waszego providera, każdy prowadzi przez własny flow (kilka rekordów DNS, weryfikacja w panelu). Kiedy domena jest zweryfikowana i macie dane logowania SMTP (host, port, nazwa użytkownika, hasło), wracacie do .env remark42:
SMTP_HOST=smtp.example-provider.com
SMTP_PORT=587
SMTP_USERNAME=<login_z_panelu_providera>
SMTP_PASSWORD=<haslo_z_panelu_providera>
SMTP_TLS=false
SMTP_STARTTLS=true
SMTP_TIMEOUT=10s
Port 587 z STARTTLS jest standardem. Jeżeli provider każe użyć 465 z implicit TLS, zamieńcie flagi:
SMTP_PORT=465
SMTP_TLS=true
SMTP_STARTTLS=false
Jedna z flag SMTP_TLS lub SMTP_STARTTLS musi być na true, druga na false. Port 25 pomijamy, VPS-y i większość providerów blokuje go jako źródło spamu.
Po restarcie docker compose up -d remark42 sprawdzicie w logach, że kanał email został podniesiony. remark42 loguje status każdego kanału notyfikacji przy starcie:
docker compose logs -f remark42 | grep -i "smtp\|notify\|email"
Komunikaty typu smtp auth failed, connection refused albo tls handshake failed wskazują na problem w danych logowania, porcie albo trybie TLS. Poprawka polega na porównaniu wartości w .env z tym, co dał Wam provider.
Magic-link email jako trzeci kanał logowania#
Magic-link to sposób logowania bez hasła i bez OAuth. Użytkownik podaje email, dostaje jednorazowy token, wpisuje go w widgecie, backend weryfikuje i wystawia sesję. Trzeci kanał obok GitHub OAuth i anonima.
Mechanika i kiedy ma sens#
Krok po kroku od strony backendu wygląda tak. Czytelnik wpisuje adres w widgecie, wybiera „Sign in with Email". Backend generuje jednorazowy token, zapisuje w bazie z krótkim TTL (domyślnie piętnaście minut) i przypisaniem do konkretnego adresu. Do czytelnika idzie mail z tokenem, w nowszych wersjach remark42 tokenem gołym, bez klikalnego linka.
Rezygnacja z linka nie jest przypadkowa. Klienty mailowe typu Outlook wykonują prefetch linków w celu skanowania antywirusowego, przez co konsumują token, zanim czytelnik zdąży kliknąć. Wysyłanie samego tokena eliminuje tę klasę awarii, kosztem ręcznego kopiowania i wklejenia po stronie czytelnika.
Czytelnik kopiuje token, wraca do widgetu i wkleja w polu obok „Sign in". Widget wysyła token do backendu, ten sprawdza, czy istnieje, nie wygasł i nie był już użyty. Jeżeli wszystko się zgadza, backend wystawia sesję i zaznacza token jako zużyty.
Konfiguracja jest banalna, o ile SMTP już działa:
AUTH_EMAIL_ENABLE=true
AUTH_EMAIL_FROM=login@example.com
AUTH_EMAIL_SUBJ=Zaloguj do komentarzy
Trzy linie, restart, w widgecie pojawia się dodatkowa opcja email obok GitHuba i anonima. Blok SMTP_* skonfigurowany wcześniej dla powiadomień jest w pełni ponownie wykorzystywany.
Pułapka konfliktu z menedżerami haseł#
Setup przetestowany, wysyłacie sobie testowy mail, wklejacie token do widgetu i zauważacie coś dziwnego. Formularz z tokenem znika, popup się zamyka bez komunikatu, kliknięcie „Sign in" pokazuje puste pole, próba ponownego wklejenia znowu zamyka formularz. Konsola przeglądarki pusta, żadnego JavaScript-owego błędu. Zakładka Network pusta, żaden request do backendu nie leci. Widget nie próbuje się nawet zalogować, tylko cicho resetuje swój stan.
Diagnoza wychodzi w trybie incognito. Bez rozszerzeń przeglądarki magic-link działa poprawnie, popup zamyka się po autoryzacji i widget pokazuje zalogowaną tożsamość. W normalnym profilu cichy fail wraca. Winowajca: menedżer haseł. Bitwarden, 1Password, LastPass i podobne rozszerzenia są zaprogramowane tak, żeby przechwytywać zdarzenie paste w polach, które wyglądają na wpisywanie kodów jednorazowych (krótki input, autocomplete=off). Rozszerzenie łapie event, zanim widget zdąży go obsłużyć, zamyka overlay w przekonaniu, że to jego dialog, i paste nigdy nie dociera do handlera JavaScript-owego widgetu.
Cichy fail nie zostawia komunikatu błędu, logu ani sygnału dla czytelnika, że coś nie zadziałało. Admin bloga zdiagnozuje to sam, ale zwykły czytelnik przełknie cichy fail i pójdzie sobie bez komentarza.
Decyzje produktowe które musisz podjąć sam#
Konfiguracja techniczna to jedno, decyzje o tym, jak traktujecie czytelników, to drugie. Trzy takie decyzje wychodzą wprost z konfiguracji auth i powiadomień. Każda z nich to trade-off, nie oczywisty wybór.
AUTH_ANON: filtr antyspamowy czy niska bariera#
Anonimowe komentarze (AUTH_ANON=true) obniżają barierę wejścia do zera. Czytelnik bez konta wybiera dowolny nick i od razu komentuje. Cena: nick nie jest zarezerwowany dla konta, więc podszywanie się pod znane nicki jest banalne. Ban nie działa, bo zbanowany wraca pod innym nickiem. Spam ma niską barierę, bot podaje losowy nick i wrzuca reklamę, a remark42 nie ma sensownego mechanizmu blokowania.
Zamknięcie anona (AUTH_ANON=false, tylko GitHub) daje odwrotny profil. Każdy komentujący ma realną tożsamość, awatar, powiązanie z profilem. Filtr antyspamowy jest darmowy: bot musiałby założyć konto GitHub, żeby spamować, koszt niewspółmierny do wartości. Ban działa, bo tożsamość jest przypięta do konta. Cena: odcinacie osoby bez konta GH i tych, którzy nie chcą dawać dostępu przez OAuth tylko po to, żeby napisać zdanie.
Bez danych o realnym natężeniu spamu decyzja opiera się na przeczuciu. Rozsądniej zostawić AUTH_ANON=true na miesiąc, obserwować proporcje komentarzy, ilość spamu i częstość podszywania, potem decydować. Przełącznik później to jedna linia w .env i restart.
Format adresu w polu From#
Konwencja mówi, żeby ustawić NOTIFY_EMAIL_FROM=no-reply@example.com, sygnał dla czytelnika „nie odpowiadaj, to zautomatyzowane". Konwencja jest jednak zła w kontekście bloga osobistego.
Providerzy pocztowi (Gmail, Outlook, Yahoo) mają statystycznie wyższą podejrzliwość wobec adresów no-reply@ i noreply@, ponieważ tego adresu masowo używają spamerzy oraz marketing. Do tego zamykacie jedyny naturalny kanał zwrotny: ktoś dostaje powiadomienie, chce coś zapytać, klika „Odpowiedz" w kliencie pocztowym, a mail wraca jako bounce albo trafia w próżnię.
Alternatywa komentarze@example.com albo blog@example.com obchodzi obie sprawy. Skrzynka nie musi być realna, bounce z „user unknown" jest mniej agresywny komunikacyjnie niż no-reply. Jeżeli chcecie kontakt zwrotny, konfigurujecie forwarder na Wasz osobisty adres.
Magic-link mimo pułapki z password managerami#
Cichy fail magic-linka z menedżerami haseł stawia przed decyzją bez czystego zwycięzcy. Poniżej trzy realistyczne ścieżki z argumentami za i przeciw.
Zostawić włączony, licząc się z tym, że część czytelników wpada w cichy fail. Argument: pełne pokrycie kanałów logowania. Argument przeciw: część czytelników odbije się bez komunikatu i bez zgłoszenia problemu.
Wyłączyć magic-link (usunąć AUTH_EMAIL_ENABLE=true z .env), zostawić tylko GitHub i anonim. Argument: każda opcja działa niezawodnie. Argument przeciw: rezygnujecie z sensownego kanału dla osób bez konta GH.
Zostawić i dodać notkę pod widgetem typu „przy logowaniu emailem wyłączcie menedżera haseł dla tej strony". Argument: uczciwe wobec czytelnika, pokazuje wiedzę o problemie. Argument przeciw: prosicie użytkownika o dodatkową robotę, mało kto ją wykona.
Bez danych trudno wybrać na pewno. Konkretną decyzję warto podjąć po miesiącu obserwacji, ile osób próbuje kanału emailowego i ile z nich się przebija.
Podsumowanie#
Konfiguracja OAuth przez GitHuba i powiadomień mailowych w remark42 sprowadza się do przepisania kilku wartości z paneli providerów do .env i restartu kontenera. Sam kod remark42 jest deklaratywny: zmienne włączają kanały, restart podnosi konfigurację, widget dopina się do interfejsu automatycznie.
Uwaga na kilka rzeczy, które nie są oczywiste z dokumentacji:
- Callback URL w OAuth App musi mieć dokładny format
https://<REMARK_URL>/auth/<provider>/callbackbit po bicie,redirect_uri_mismatchto najczęstszy błąd setupu. - Subskrypcje użytkowników są opt-in dwuetapowym, nie automatyczne, i to jest funkcja, nie brak.
- Magic-link email ma cichy konflikt z menedżerami haseł typu Bitwarden i 1Password, decyzja o jego włączeniu powinna być świadoma.
Trade-offy produktowe (AUTH_ANON, format From:, magic-link tak/nie) nie mają jedynego słusznego wyboru. Warto podejmować je z pełną świadomością konsekwencji, a decyzje weryfikować danymi po kilku tygodniach, kiedy proporcje kanałów i wolumen spamu przestają być teoretyczne.
Źródła i dalsze materiały#
- remark42 backend parameters
– oficjalna referencja wszystkich zmiennych środowiskowych (
AUTH_*,NOTIFY_*,SMTP_*) - remark42 authorization – dokumentacja auth providerów wspieranych przez remark42 (OAuth, magic-link email, anonim)
- Creating an OAuth App on GitHub – dokumentacja GitHuba, formalne kroki rejestracji aplikacji
- Self-hosting remark42 obok istniejącego Caddy na VPS – pierwszy wpis w serii, setup instancji na VPS
$ komentarze --wczytaj