W poprzednim artykule o Astro Actions rzucaliśmy z akcji ActionError({ code: 'UNAUTHORIZED' }), gdy użytkownik nie był zalogowany. Ale skąd akcja wie, czy ktoś jest zalogowany? Odpowiedź to dwie współpracujące warstwy: middleware, które rozpoznaje użytkownika przy każdym żądaniu, i biblioteka uwierzytelniania, która zarządza sesją i logowaniem. Zaczniemy od middleware, bo to fundament.
Middleware w Astro — jeden punkt na trasie każdego żądania
to plik src/middleware.ts, który
eksportuje funkcję onRequest. Astro wykrywa go automatycznie, nie ma osobnego
włączania w konfiguracji. Funkcja dostaje dwa argumenty: context (z danymi
żądania) i next (kontynuacja przetwarzania):
Funkcja pomocnicza daje pełne typowanie context i next, bo bez niej musiałbyś typować argumenty ręcznie.
Każde renderowanie przechodzi przez ten jeden punkt: podczas procesu budowania tras prerenderowanych albo przy żądaniu do tras renderowanych na żądanie. To czyni middleware dobrym miejscem na bramkę autoryzacji w części serwerowej, czyli sprawdzasz raz, centralnie, zamiast powtarzać warunek na każdej chronionej stronie.
context.locals — przeniesienie użytkownika do stron
Sprawdzenie sesji w middleware jest bezużyteczne, jeśli wynik nie dotrze do strony. Od tego jest — obiekt, który żyje przez cały cykl pojedynczego żądania:
Żeby TypeScript wiedział, co siedzi w locals, deklarujesz typ raz w env.d.ts, czyli mamy tu tą samą filozofię typowania u źródła, co przy w przypadku typowanych Content Collections z Zod:
Teraz na dowolnej stronie .astro masz otypowany dostęp:
To samo Astro.locals.user jest dostępne w Astro Actions przez context.locals i właśnie stąd akcja wie, czy rzucić UNAUTHORIZED.
Uwierzytelnianie to nie autoryzacja
Sesja pozwala zidentyfikować użytkownika (czyli kto wysłał żądanie), ale nie rozstrzyga o jego uprawnieniach do konkretnej operacji. O ile przekierowanie niezalogowanej osoby z /panel jest jedynie procesem uwierzytelniania, o tyle weryfikacja roli, członkostwa w organizacji czy prawa własności do rekordu stanowi autoryzację i musi być każdorazowo przeprowadzana na poziomie dostępu do danych.
Dlatego middleware jest dobrą bramką dla całych sekcji, ale nie zastępuje kontroli uprawnień w Astro Actions i endpointach. Obsługa żądania zmieniającego fakturę powinna pobrać użytkownika z context.locals, a następnie ograniczyć zapytanie także identyfikatorem właściciela lub organizacji. Samo ukrycie strony nie chroni bezpośredniego wywołania operacji.
Ochrona ścieżek w Astro — bramka autoryzacji w jednym miejscu
Mając użytkownika w locals, bramkę dla całych sekcji stawiasz w middleware, nie na każdej stronie z osobna:
Jedna lista, jedno sprawdzenie, jedna decyzja — dodanie nowej chronionej ścieżki to dopisanie wpisu do tablicy, a nie pamiętanie o warunku na każdej nowej stronie.
Trzy detale w tym kodzie są nieprzypadkowe. Dopasowanie path === p || path.startsWith(p + '/') chroni /panel i wszystko poniżej, ale nie łapie przypadkiem /panel-pomocy, które gołe startsWith('/panel') też by objęło. Wyłączenie /api/auth zapobiega scenariuszowi, w którym bramka blokuje sam mechanizm logowania — callback OAuth nigdy nie ma sesji, bo dopiero ją tworzy. A rozdzielenie odpowiedzi dla API i stron bierze się stąd, że middleware w Astro obsługuje także endpointy: fetch z klienta, który zamiast JSON-a z 401 dostanie przekierowanie i HTML strony logowania ze statusem 200, to klasyczne źródło trudnych do debugowania błędów parsowania.
sequence() — łańcuch middleware zamiast monolitu
Realna aplikacja potrzebuje więcej niż uwierzytelniania: logowanie żądań, i18n, nagłówki bezpieczeństwa. Wrzucanie wszystkiego do jednego onRequest szybko robi się nieczytelne. rozdziela odpowiedzialności:
Middleware wykonują się w kolejności na żądaniu, a odpowiedzi wracają w odwrotnej — jak warstwy cebuli. Każda funkcja robi jedną rzecz i da się ją testować w izolacji.
Uwierzytelnianie w 2026: Better Auth zamiast biblioteki Lucia v3
Od razu zaznaczam, że Lucia v3, która przez lata była popularnym rozwiązaniem uwierzytelniania dla Astro, została zdeprecjonowana jako biblioteka w marcu 2025 roku. W związku z tym adaptery przestały być rozwijane już pod koniec 2024 roku, a jej twórca świadomie przekształcił Lucię w przewodnik architektoniczny. Jest on nadal cenny jako nauka wzorców (sesje, tokeny, OAuth), ale pakietu lucia nie instalujesz jako aktywnie utrzymywanej zależności w nowym projekcie.
Astro nie ma jednej oficjalnej biblioteki uwierzytelniania. W 2026 roku jednym z głównych wyborów z bezpośrednim wsparciem Astro jest :
- TypeScript-first — pełne typowanie sesji i użytkownika, bez ręcznego deklarowania.
- Oficjalne wtyczki 2FA, passkeys i organizacji — funkcje, które w prostszych bibliotekach dokładasz samodzielnie.
- System wtyczek i aktywny rozwój.
Alternatywami są m.in. Clerk, Supabase Auth, Firebase Authentication lub własna implementacja sesji oparta na aktualnym przewodniku Lucii. Wybór zależy od tego, czy chcesz utrzymywać dane i przepływy uwierzytelniania samodzielnie, czy korzystać z zarządzanej usługi. W tym przykładzie używamy Better Auth, bo ma bezpośredni przewodnik integracji z Astro i działa na standardowych obiektach Request/Response.
Konfiguracja Better Auth — od zera do działającej sesji
Teraz przejdźmy do kompletnej konfiguracji. Better Auth ma jedno źródło prawdy, a jest nim plik lib/auth.ts, w którym łączysz bibliotekę z bazą i włączasz metody logowania. W tym przykładzie używamy bazy libSQL (lokalny SQLite podczas programowania, Turso na produkcji) obsługiwanej przez , więc potrzebujesz Better Auth, adaptera Drizzle i samego ORM:
Następnie konfigurujesz drizzleAdapter z providerem sqlite:
Metodę auth.api.getSession, z której korzystałeś już w middleware, wywołujesz z tej samej instancji. Better Auth pobiera secret oraz baseURL bezpośrednio ze zmiennych BETTER_AUTH_SECRET i BETTER_AUTH_URL w pliku .env. Pamiętaj, że sekret powinien mieć co najmniej 32 znaki i wysoką entropię, a na produkcji zawsze definiuj baseURL jawnie, ponieważ odpowiada on m.in. za poprawne działanie callbacków OAuth.
Pamięć podręczna sesji w ciasteczku — bez zapytania do bazy przy każdym żądaniu
Zanim pójdziemy dalej, jedna rzecz, którą widać dopiero pod obciążeniem. Skoro middleware wywołuje getSession przy każdym żądaniu renderowanym na żądanie, to każde wejście na dowolną stronę oznacza zapytanie do bazy i to także na stronach publicznych, gdzie użytkownika sprawdzasz tylko po to, żeby pokazać „Zaloguj" albo avatar. Przy libSQL na tym samym serwerze to pomijalne, ale przy bazie dostępnej przez sieć (Turso, Postgres u innego dostawcy) dokładasz dodatkowe zapytanie sieciowe do każdego żądania.
Better Auth rozwiązuje to wbudowaną pamięcią podręczną sesji w ciasteczku (cookieCache): podpisane dane sesji lądują w krótkotrwałym ciasteczku i przez skonfigurowany czas getSession czyta je stamtąd, nie dotykając bazy:
Przyjmij świadomy kompromis, w którym przez czas określony w maxAge sesja, która powinna być już nieważna (np. po wylogowaniu na innym urządzeniu lub zablokowaniu konta), może być nadal traktowana jako aktywna. Z tego powodu ustawiaj maxAge na krótki okres (liczony w minutach), a przy operacjach krytycznych, takich jak zmiana hasła czy realizacja płatności, zawsze wymuszaj weryfikację poprzez bezpośrednie odpytanie bazy danych.
Wymuszenie świeżego odczytu nie polega na drugim zwykłym wywołaniu getSession, bo ono nadal może użyć pamięci podręcznej ciasteczka. Przekaż jawnie disableCookieCache: true:
Domyślna strategia compact podpisuje dane z pamięci podręcznej, ale ich nie szyfruje. Klient nie może ich zmienić bez unieważnienia podpisu, może je jednak odczytać. Nie dodawaj więc do obiektu sesji sekretów. W sytuacji kiedy jest wymagana poufność zawartości ciasteczka, Better Auth udostępnia strategię jwe, co też oznacza jego większy rozmiar.
Trasa catch-all i migracje
Better Auth nie wpina się do tras automatycznie, ponieważ potrzebuje jednej trasy catch-all, która obsłuży logowanie, adresy zwrotne OAuth i wylogowanie pod /api/auth/*:
Jeśli cały projekt ma output: 'server', eksport prerender = false jest zbędny. Przy domyślnym output: 'static' pozostaw go w pliku i skonfiguruj adapter, żeby Astro nie próbowało wygenerować endpointu podczas buildu.
Narzędzie wiersza poleceń Better Auth generuje schemat Drizzle wymagany przez aktualną konfigurację i wtyczki. Samo wygenerowanie pliku nie zmienia jednak bazy, bo migrację tworzysz i wykonujesz narzędziami Drizzle:
Powtórz ten proces po zmianie konfiguracji uwierzytelniania, która dodaje pola lub tabele, np. po włączeniu wtyczki organizacji albo 2FA. W repozytorium zachowaj zarówno wygenerowany schemat, jak i pliki migracji.
OAuth i sesje — pełny obieg logowania
Mając konfigurację i endpoint, logowanie społecznościowe to jedno wywołanie po stronie klienta. Better Auth ma osobny klient (auth-client.ts), którego używasz w komponentach interaktywnych, np. na przycisku „Zaloguj przez GitHub":
W panelu dostawcy OAuth musisz wcześniej zarejestrować dokładny adres zwrotny Better Auth, np. https://example.com/api/auth/callback/github. callbackURL: '/panel' z kodu poniżej oznacza stronę docelową po zakończeniu logowania, a nie adres zwrotny rejestrowany u GitHuba. To dwa różne adresy.
Reszta dzieje się sama. Pełny obieg wygląda tak — i to właśnie middleware spina go ze stronami:
- Logowanie.
signIn.socialprzekierowuje do GitHuba i po zgodzie użytkownika dostawca wraca na/api/auth/callback/github, którą obsługuje trasa catch-all. Better Auth tworzy sesję, zapisuje ją w bazie i ustawia ciasteczko. - Każde kolejne żądanie. Middleware czyta ciasteczko, woła
getSession, wstawia użytkownika docontext.locals. Twoja bramka ze ścieżek już go widzi. - Strony i akcje czytają
Astro.locals.user, nie znając przy tym szczegółów sesji ani tego, czy zalogowanie poszło hasłem, czy poprzez OAuth.
Wylogowanie jest symetryczne — signOut kasuje sesję w bazie i czyści ciasteczko, więc przy następnym żądaniu middleware ustawi user na null:
Bezpieczeństwo — czego pilnować
Nie twórz własnego mechanizmu uwierzytelniania. Sesje, hashowanie haseł i OAuth to obszary, gdzie bardzo łatwo o luki. Jeśli nie posiadasz odpowiedniego doświadczenia w projektowaniu takich mechanizmów, użyj sprawdzonej biblioteki lub zarządzanej usługi.
Middleware to nie jedyna bramka. Astro Actions i endpointy też sprawdzają uprawnienia. Middleware chroni nawigację, ale mutacje danych weryfikuj dodatkowo w handlerze. Gdy obok zalogowania dochodzą role i uprawnienia, sięgnij po wzorce z artykułu o RBAC w Next.js — różnica frameworka jest kosmetyczna, model autoryzacji ten sam.
Ciasteczka sesji:
HttpOnly,Secure,SameSite. Dobra biblioteka ustawia je domyślnie. Zweryfikuj, że tak jest, zwłaszcza na produkcji po HTTPS.Ogranicz pochodzenie żądań przeciw . Better Auth domyślnie blokuje żądania spoza znanych domen, dlatego na produkcji musisz jawnie wskazać zaufane adresy w tablicy
trustedOrigins. Nie wpisuj tam ścieżek adresu zwrotnego OAuth, bo dokładny adres zwrotny konfigurujesz w panelu dostawcy. Nie wyłączajdisableCSRFCheckanidisableOriginCheckw produkcji.Waliduj przekierowanie po zalogowaniu. Parametr
?from=z URL-a może zostać podmieniony na adres zewnętrzny — dopuszczaj tylko ścieżki względne w obrębie swojej domeny, np. warunkiemfrom.startsWith('/') && !from.startsWith('//').Ogranicz tempo prób logowania. Endpointy
/api/auth/*to naturalny cel ataków brute force i credential stuffingu. Better Auth ma wbudowany (domyślnie włączony na produkcji, zaostrzony dla tras logowania) — zweryfikuj jego konfigurację, a przy deploymentach za proxy upewnij się, że biblioteka widzi prawdziwe IP klienta, nie adres load balancera. Ufaj tylko nagłówkowi nadpisywanemu przez własne proxy, np.CF-Connecting-IP; nagłówek przesłany bezpośrednio przez klienta można sfałszować.Dobierz trwały magazyn rate limitera. Better Auth domyślnie przechowuje liczniki w pamięci procesu. Na serverless i przy wielu instancjach taki limit nie jest globalny i zeruje się wraz z instancją — użyj bazy albo
secondaryStorage, jeśli limiter ma skutecznie obejmować cały deployment.
