Przejdź do treści

Bezpieczna autentykacja w React: Gdzie trzymać token?

Zapomnij o trzymaniu tokenów w localStorage. Zobacz, jak bezpiecznie wdrożyć ciasteczko httpOnly, obsłużyć odświeżanie tokenu oraz wiele kart przeglądarki.

Maciej Sala

Founder StriveLab

11 min czytaniaOpublikowano 24 kwietnia 2026 (Aktualizacja 14 lipca 2026)

Dwa modele zagrożeń w autentykacji aplikacji React

Zanim zaczniesz wybierać, zrozum przed czym się bronisz.

— atakujący wstrzykuje swój JavaScript na Twoją stronę (np. przez niezabezpieczone pole formularza, przesłane treści użytkowników albo zewnętrzny skrypt, który został skompromitowany). Gdy to się uda, może odczytać wszystko, co JavaScript widzi, czyli localStorage, sessionStorage, zmienne w pamięci oraz tokeny.

, czyli atak, w którym atakujący zmusza przeglądarkę użytkownika do wysłania żądania do Twojej aplikacji wraz z jego ciasteczkami. Użytkownik jest zalogowany, atakujący ma stronę, która robi <img src="https://yourapp.com/api/transfer?to=attacker&amount=1000">. Jeśli Twoja aplikacja ufa cookie automatycznie wysyłanemu przez przeglądarkę: cyk, przelew został wykonany.

XSS dotyczy Twojej strony, czyli kod wykonuje się w kontekście Twojego originu. CSRF dotyczy obcej strony, która wykorzystuje fakt, że użytkownik jest zalogowany do Ciebie. To dwa różne fronty i nie ochronisz ich jednym zabezpieczeniem.

model zagrożeń

httpOnly cookie nie pozwala JavaScriptowi odczytać tokenu, ale nie eliminuje skutków XSS: złośliwy kod nadal może wywołać punkty końcowe jako zalogowany użytkownik i odczytać dane dostępne stronie. Cookie wysyłane automatycznie wymaga też ochrony przed CSRF.

localStorage jest podatne na XSS, ale nie na CSRF (atakujący musiałby mieć kontrolę nad Twoim origin).

Gdzie trzymać token w aplikacji React?

1. Token w localStorage

Code
// Zapisz
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5c...')
 
// Odczyt
const token = localStorage.getItem('token')
 
// Użycie w fetch
fetch('https://api.example.com/me', {
  headers: { Authorization: `Bearer ${token}` },
})

Zalety

  • Prostota.
  • Token przetrwa odświeżenie strony.
  • Działa w każdej przeglądarce.

Wady

  • XSS = koniec gry. Każdy skrypt na Twojej stronie może odczytać token i wysłać do atakującego i to nawet jeśli dzisiaj masz bezpieczny kod. Wystarczy jedna skompromitowana zależność, np złośliwa aktualizacja popularnej paczki npm. Aktualnie to klasyczny wektor ataku na łańcuch dostaw.

  • Tokeny przetrwają nawet po wylogowaniu, jeśli zapomnisz jawnie ich usunąć.

Podsumowując, unikaj dla tokenów z długim TTL. Krótki access token w pamięci przeglądarki bywa akceptowalny tylko w prostych SPA bez własnego zaplecza i przy świadomie zaakceptowanym ryzyku XSS. Jeśli masz własny serwer, lepszym punktem wyjścia jest access token w pamięci procesu przeglądarki oraz refresh token w httpOnly cookie.

2. Token w sessionStorage

Praktycznie identyczne jak localStorage pod kątem XSS, ale znika przy zamknięciu karty. Ogranicza trwałość tokenu, nie jest jednak ochroną przed kodem wykonanym w tej samej karcie.

Code
// Serwer ustawia
response.setHeader(
  'Set-Cookie',
  `__Host-session=${sessionId}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600`,
)

Zalety:

  • JavaScript nie ma dostępu. XSS nie wykradnie tokenu.

  • Automatyczne wysyłanie w żądaniach (nie musisz ręcznie dodawać nagłówka).

Wady:

  • CSRF. Przeglądarka automatycznie wysyła cookie, a atakujący z innej strony może wymusić akcję jako zalogowany użytkownik.

  • Wymaga serwera pod kontrolą. SPA i API na różnych originach mogą działać przez CORS z credentials, ale wymagają precyzyjnej konfiguracji originów i bywają ograniczane przez politykę ciasteczek zewnętrznych. Prostszym wariantem jest wspólna witryna albo BFF.

  • Problemy z SSR w Next.js, czyli cookie jest stringiem na serwerze, trzeba parsować i przekazywać.

Ochrona przed CSRF:

  • SameSite=Strict (lub świadomie dobrane Lax), czyli bardzo ważna warstwa ochrony, ale też nie do końca, bo nie zastępuje walidacji dla punktów końcowych zmieniających stan, a Lax ma wyjątki dla części nawigacji.
  • CSRF token albo kontrola originu, czyli dla mutacji serwer weryfikuje token przesłany w niestandardowym nagłówku lub poprawny Origin / Referer; warto też odrzucać żądania cross-site przez Sec-Fetch-Site. Pamiętaj, by nie używać GET do operacji zmieniających dane.

Werdykt: Rekomendowane dla refresh tokenów i dla aplikacji z własnym serwerem. Wymaga poprawnej ochrony CSRF oraz ochrony aplikacji przed XSS.

4. Token w pamięci w zmiennej JavaScript

Code
// Gdzieś w aplikacji (np. Zustand store lub React Context)
let accessToken: string | null = null
 
export function setAccessToken(token: string) {
  accessToken = token
}
 
export function getAccessToken() {
  return accessToken
}

Zalety

  • XSS jest trudniejsze. Atakujący musi wykonać kod w momencie, gdy zmienna istnieje, nie może po prostu odczytać trwałej pamięci przeglądarki.

  • Token znika przy odświeżeniu strony, więc okno ataku jest mniejsze.

Wady

  • Odświeżenie strony = wylogowanie. Użytkownik musi się ponownie logować przy każdym odświeżeniu.

  • Problem wielu kart. Każda karta ma swoją kopię, a logowanie w jednej karcie nie dotyczy drugiej.

Podsumowujac, dobry dla access tokenów w kombinacji z refresh tokenem w httpOnly cookie. Zobacz wzorzec, który jest niżej.

Access token i refresh token: zalecany wzorzec logowania

To częsty, rozsądny wzorzec dla SPA z własnym API, ale nie jedyny właściwy. Jeżeli przeglądarka nie musi widzieć access tokenu, prostsza sesja serwerowa lub BFF z cookie zmniejsza ilość kodu po stronie klienta.

  1. Access token. Krótki TTL (5–15 min), trzymany w pamięci procesu przeglądarki.
  2. Refresh token. Długi TTL (7-30 dni), trzymany w httpOnly cookie.
  3. Ciche odświeżenie tokenu (silent refresh), gdy access token wygaśnie, klient wysyła żądanie do punktu końcowego w formacie https://api.example.com/refresh, serwer sprawdza refresh token z cookie i zwraca nowy access token.
Diagram
Pełny cykl: logowanie wydaje obie kredencje, wygaśnięcie access tokenu uruchamia ciche odświeżenie z rotacją refresh tokenu.

Warto ograniczyć zasięg refresh cookie przez Path=/refresh (albo pełną ścieżkę punktu końcowego), aby przeglądarka nie wysyłała go do pozostałych punktów końcowych. To dodatkowa warstwa ochrony, a nie granica bezpieczeństwa: nie zastępuje HttpOnly, walidacji CSRF ani poprawnej konfiguracji logów po stronie serwera.

Implementacja access token i refresh token w React

Code
// lib/auth-store.ts
import { create } from 'zustand'
 
type AuthStore = {
  accessToken: string | null
  user: User | null
  setAuth: (token: string, user: User) => void
  clearAuth: () => void
}
 
// Przykładowy minimalny model. W aplikacji użyj właściwego typu domenowego.
type User = { id: string; email: string }
 
export const useAuthStore = create<AuthStore>((set) => ({
  accessToken: null,
  user: null,
  setAuth: (accessToken, user) => set({ accessToken, user }),
  clearAuth: () => set({ accessToken: null, user: null }),
}))
Code
// lib/api-client.ts
import { useAuthStore } from './auth-store'
 
// Single-flight: jedna trwająca próba odświeżenia tokenu naraz w bieżącej karcie.
let refreshPromise: Promise<string> | null = null
 
export function refreshAccessToken() {
  refreshPromise ??= withCrossTabRefreshLock(doRefresh).finally(() => {
    refreshPromise = null
  })
  return refreshPromise
}
 
async function withCrossTabRefreshLock<T>(work: () => Promise<T>) {
  // Refresh cookie jest współdzielony przez karty, więc Promise na poziomie modułu nie
  // wystarcza. Web Locks serializuje odświeżenie między kartami tego originu.
  if (typeof navigator === 'undefined' || !navigator.locks) return work()
  return navigator.locks.request('auth-refresh', { mode: 'exclusive' }, work)
}
 
export function getCsrfHeaders() {
  // Token jest wydawany przez serwer podczas startu sesji i wiązany z sesją.
  const token = document
    .querySelector('meta[name="csrf-token"]')
    ?.getAttribute('content')
 
  if (!token) throw new Error('Missing CSRF token')
  return { 'X-CSRF-Token': token }
}
 
async function doRefresh() {
  const response = await fetch('https://api.example.com/refresh', {
    method: 'POST',
    credentials: 'include', // wyślij httpOnly cookie
    headers: getCsrfHeaders(),
  })
 
  if (!response.ok) {
    useAuthStore.getState().clearAuth()
    throw new Error('Session expired')
  }
 
  const { accessToken, user } = await response.json()
  useAuthStore.getState().setAuth(accessToken, user)
  return accessToken
}
 
export async function apiFetch(url: string, options: RequestInit = {}) {
  let token = useAuthStore.getState().accessToken
 
  // Jeśli brak tokenu, spróbuj odświeżenia (np. po przeładowaniu strony).
  if (!token) {
    token = await refreshAccessToken().catch(() => null)
  }
 
  const method = (options.method ?? 'GET').toUpperCase()
  const canRetry = ['GET', 'HEAD', 'OPTIONS'].includes(method)
  const requestWithToken = (accessToken: string | null) => {
    const headers = new Headers(options.headers)
    if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`)
    return new Request(url, { ...options, headers })
  }
 
  let response = await fetch(requestWithToken(token))
 
  // Automatycznie ponawiaj wyłącznie bezpieczne metody. Mutacje powinny mieć
  // idempotency key i świadomą obsługę błędów po stronie wywołującej.
  if (response.status === 401 && canRetry) {
    token = await refreshAccessToken()
    response = await fetch(requestWithToken(token))
  }
 
  return response
}

Zatrzymaj się przy refreshPromise, bo to najczęściej pomijany element tego wzorca. Panel może wysłać pięć żądań naraz, wszystkie dostaną 401 i bez single-flight każde uruchomi własne odświeżenie tokenu. Przy rotacji pierwsze odświeżenie unieważnia stary token, a następne wygląda dla serwera jak ponowne użycie zużytego tokenu i może zamknąć rodzinę sesji.

Sama współdzielona obietnica wystarcza tylko w jednej karcie. Refresh cookie jest wspólny dla wszystkich kart, dlatego przykład używa też navigator.locks: serializuje odświeżenia między kartami tego samego originu. Dla przeglądarek bez Web Locks trzeba zapewnić odpowiednik po stronie architektury, np. sesję BFF bez access tokenu w JS, zamiast udawać, że modułowa zmienna rozwiązuje problem globalnie.

Punkt końcowy /refresh i punkty końcowe logowania powinny zwracać Cache-Control: no-store. Serwer ma atomowo rotować token, przechowywać po stronie bazy jego bezpieczny skrót oraz wykrywać użycie tokenu, który został już zastąpiony.

Pierwsze ładowanie i hydration po odświeżeniu aplikacji

Po przeładowaniu strony access token znika, więc przy montowaniu aplikacji próbujemy odświeżyć sesję:

Code
// app/providers.tsx
'use client'
 
import { type ReactNode, useEffect, useState } from 'react'
import { refreshAccessToken } from '@/lib/api-client'
import { onAuthChange } from '@/lib/auth-sync'
import { useAuthStore } from '@/lib/auth-store'
 
export function AuthProvider({ children }: { children: ReactNode }) {
  const [ready, setReady] = useState(false)
 
  useEffect(() => {
    let mounted = true
    void refreshAccessToken()
      .catch(() => {}) // nie ma sesji — OK, użytkownik jest anonimowy
      .finally(() => {
        if (mounted) setReady(true)
      })
 
    const unsubscribe = onAuthChange((event) => {
      if (event.type === 'logout') {
        useAuthStore.getState().clearAuth()
      } else {
        // Inna karta dostała nowe cookie przy logowaniu. Blokada z api-client
        // zapobiega wyścigowi z równoległym odświeżeniem.
        refreshAccessToken().catch(() => useAuthStore.getState().clearAuth())
      }
    })
 
    return () => {
      mounted = false
      unsubscribe()
    }
  }, [])
 
  if (!ready) return <SplashScreen />
  return children
}

Jeśli użytkownik miał aktywną sesję (refresh token w cookie nadal ważny), dostaje nowy access token i jest zalogowany. Jeśli nie, zobaczy publiczną treść jako osoba anonimowa.

Automatyczne ponowienie w apiFetch dotyczy wyłącznie metod bezpiecznych. Dla POST, PUT, PATCH i DELETE serwer najpierw musi zweryfikować uprawnienia, a klient powinien używać klucza idempotencji tam, gdzie podwójne wykonanie byłoby kosztowne, np. przy płatności lub wysłaniu formularza.

Integracja autentykacji z TanStack Query

Code
import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '@/lib/api-client'
 
function useUser() {
  return useQuery({
    queryKey: ['user', 'me'],
    queryFn: async () => {
      const response = await apiFetch('https://api.example.com/me')
      if (!response.ok) throw new Error('Unauthorized')
      return response.json()
    },
    retry: (failureCount, error) => {
      // Nie próbuj ponownie dla błędów autentykacji.
      if (error.message === 'Unauthorized') return false
      return failureCount < 2
    },
  })
}

apiFetch obsługuje odświeżanie tokenu automatycznie, więc TanStack Query nie musi znać logiki autentykacji. To czysty podział odpowiedzialności. Szczegóły TanStack Query opisuję w osobnym artykule.

Synchronizacja logowania między kartami przeglądarki

Problem: użytkownik ma otwartych 5 kart. Loguje się w jednej, a pozostałe cztery nie wiedzą, że użytkownik jest teraz zalogowany. Wylogowuje się w jednej, a cztery inne wciąż myślą, że ma dostęp.

Rozwiązanie: BroadcastChannel API.

Code
// lib/auth-sync.ts
type AuthEvent = { type: 'session-started' } | { type: 'logout' }
 
const channel =
  typeof window !== 'undefined' && 'BroadcastChannel' in window
    ? new BroadcastChannel('auth')
    : null
 
export function broadcastLogin() {
  // Nie przesyłaj access tokenu przez kanał. Inna karta zrobi własne odświeżenie.
  channel?.postMessage({ type: 'session-started' })
}
 
export function broadcastLogout() {
  channel?.postMessage({ type: 'logout' })
}
 
export function onAuthChange(callback: (event: AuthEvent) => void) {
  if (!channel) return () => {}
 
  const handler = (e: MessageEvent<AuthEvent>) => callback(e.data)
  channel.addEventListener('message', handler)
  return () => channel.removeEventListener('message', handler)
}

Obsługa zdarzeń jest już w AuthProvider z poprzedniego przykładu. Po zalogowaniu wywołaj broadcastLogin() dopiero po zapisaniu refresh cookie przez serwer. Pozostałe karty dostają jedynie sygnał, a następnie pobierają własny access token przez kontrolowane odświeżenie. Dzięki temu token nie krąży w BroadcastChannel, a blokada z przykładu wyżej nie pozwala kartom użyć jednocześnie tego samego refresh tokenu.

Wylogowanie w aplikacji React krok po kroku

Wylogowanie ma do wykonania kilka rzeczy, bo musi:

  1. Usunąć access token z pamięci.
  2. Powiedzieć serwerowi, żeby unieważnił refresh token.
  3. Wysłać sygnał wylogowania do innych kart.
  4. Przekierować do /login albo strony głównej.
Code
import { getCsrfHeaders } from '@/lib/api-client'
import { broadcastLogout } from '@/lib/auth-sync'
import { useAuthStore } from '@/lib/auth-store'
 
async function logout(onLoggedOut: () => void) {
  try {
    // 1. Powiedz serwerowi, żeby unieważnił refresh token.
    await fetch('https://api.example.com/logout', {
      method: 'POST',
      credentials: 'include',
      headers: getCsrfHeaders(),
    })
  } finally {
    // 2-4. Nawet gdy żądanie nie powiedzie się (brak sieci, 500),
    // lokalnie użytkownik MUSI zostać wylogowany
    useAuthStore.getState().clearAuth()
    broadcastLogout()
    onLoggedOut() // np. navigate('/login') albo router.push('/login')
  }
}

Blok try/finally nie jest tu ozdobą: bez niego nieudane żądanie do /logout (chwilowy brak sieci, błąd serwera) przerwałoby funkcję i użytkownik, który świadomie kliknął „Wyloguj", zostałby zalogowany dalej. Z punktu widzenia bezpieczeństwa to gorszy scenariusz niż osierocony refresh token po stronie serwera. Sam serwer powinien przy wylogowaniu usuwać refresh token z bazy, bo inaczej skradziony token działa nadal, mimo że użytkownik „się wylogował".

Limity prób i ochrona przed brute force

Kilka zasad, które stosuję niezależnie od frameworka:

  • Limit prób na punkcie logowania. Licz równocześnie próby dla konta i adresu IP, stosuj progresywne opóźnienie, a odpowiedzi zachowuj identyczne dla istniejącego i nieistniejącego konta. Sam limit dla adresu IP łatwo ominąć albo wykorzystać do blokady użytkowników za wspólnym NAT-em.
  • Hasła i reset hasła. Zapisuj wyłącznie skrót hasła (preferuj Argon2id), token resetu generuj jednorazowo, z krótkim TTL i przechowuj jego skrót. Nie ujawniaj, czy e-mail istnieje w bazie.
  • Siła hasła. Minimum 12 znaków i sprawdzanie przez @zxcvbn-ts/core zamiast ręcznych reguł, a dla kont z danymi wrażliwymi dodaj MFA lub passkeys oraz wymagaj ponownej autentykacji przed krytyczną zmianą.
  • Dziennik logowań. Kiedy użytkownik się zalogował, z jakiego IP i jakiej przeglądarki. Jeśli ta informacja jest dostępna w interfejsie, użytkownik widzi podejrzane sesje.
  • Wszystkie sesje / wylogowanie ze wszystkich urządzeń — użytkownik może unieważnić wszystkie refresh tokeny.

Frameworki i biblioteki do autentykacji w React

Jeśli nie chcesz implementować tego wszystkiego ręcznie:

Auth.js (dawne NextAuth), czyli popularne rozwiązanie dla Next.js, OAuth, linków magicznych i logowania hasłem. Możesz śmiało korzystać z jego mechanizmów sesji i ochrony tras, ale nie zakładaj, że automatycznie rozwiązuje rotację OAuth refresh tokenów. Oficjalna dokumentacja wymaga własnej implementacji i opisuje wyścig przy równoległych odświeżeniach.

Supabase Auth, czyli część platformy Supabase. Wygodne, ale wiąże Cię z całym ekosystemem Supabase (autentykację trudno potem wyjąć osobno).

Clerk. Komercyjny dostawca z bardzo dobrym doświadczeniem developerskim, posiada gotowe komponenty logowania, rejestracji i profilu. To dobry wybór pod szybkie MVP.

Auth0. Opcja korporacyjna, która jest najdroższa i najbogatsza w funkcje (SSO, SAML, zaawansowane polityki).

Dla większości projektów StriveLab używam Auth.js, bo jest darmowe, open-source i bardzo elastyczne.

Typowe błędy przy JWT, sesjach i OAuth

W audytach kodu widuję regularnie te:

1. JWT z długim TTL w localStorage. 30-dniowy token w localStorage oznacza, że po ataku XSS użytkownik może mieć ukradziony dostęp na miesiąc. Użyj access tokenu z krótkim TTL i refresh tokenu w httpOnly cookie.

2. Brak rotacji refresh tokenów. Przy każdym odświeżeniu serwer powinien wydawać nowy refresh token i unieważniać stary atomowo. Użycie już zastąpionego tokenu powinno unieważnić rodzinę sesji i zostać zapisane w audycie.

3. Sekrety w kodzie klienta. apiKey wpisany na stałe w kodzie React trafia do paczki JavaScriptu i każdy go odczyta. Wszystkie sekrety trzymaj po stronie serwera.

4. Traktowanie localStorage jako źródła prawdy. Użytkownik może zmodyfikować localStorage w DevTools. Tokeny muszą być weryfikowane po stronie serwera przy każdym żądaniu. Klient tylko przenosi token, serwer decyduje, czy użytkownik ma dostęp.

5. Brak CSRF przy httpOnly cookies. SameSite jest warstwą ochrony, nie pełnym protokołem autoryzacji żądania. Dla mutacji opartych na cookie dodaj token CSRF lub rygorystyczną kontrolę Origin oraz Sec-Fetch-Site.

6. Niedokładna walidacja JWT. Serwer musi przypinać akceptowane algorytmy i weryfikować podpis, iss, aud, exp oraz nbf. Nie ufaj danym z ładunku tylko dlatego, że token da się zdekodować.

7. OAuth traktowany jak zwykłe przekierowanie. Dla logowania przez Google czy Apple używaj Authorization Code Flow z PKCE, waliduj state i w OIDC nonce, a redirect URI porównuj dokładnie z zarejestrowaną wartością. Sekret klienta i wymianę kodu trzymaj po stronie serwera.

Lista kontrolna produkcyjna autentykacji po stronie klienta

Zanim nazwiesz autentykację „gotową", sprawdzam te rzeczy:

  • Refresh token jest w httpOnly, Secure, SameSite=Lax albo SameSite=Strict cookie.

  • Access token żyje krótko: zwykle 5-15 minut, nie 7 dni.
  • Refresh token rotuje się przy każdym odświeżeniu, a stary token jest unieważniany.

  • Serwer wykrywa ponowne użycie starego refresh tokenu i unieważnia całą rodzinę sesji.

  • Klient odświeża token w trybie single-flight — równoległe 401 nie odpalają kilku odświeżeń naraz.

  • Mutacje zmieniające dane mają ochronę CSRF obok samego cookie.

  • Wylogowanie usuwa sesję po stronie serwera, nie ogranicza się do wyczyszczenia stanu klienta.

  • Wylogowanie w wielu kartach działa przez BroadcastChannel albo zdarzenie pamięci przeglądarki.

  • Punkty końcowe logowania, odświeżania tokenu i resetu hasła mają limity prób oraz dziennik audytowy.

  • Odpowiedzi logowania i odświeżania tokenu mają Cache-Control: no-store, a tokeny nie trafiają do logów, URL-i ani narzędzi analitycznych.

  • OAuth używa Authorization Code Flow z PKCE oraz poprawnej walidacji state, nonce i redirect URI.

  • Hasła są haszowane przez Argon2id, reset hasła ma jednorazowy token i krótki TTL, a konta wrażliwe oferują MFA lub passkeys.

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.
React

Często zadawane pytania

Czy JWT to „najbezpieczniejszy" format tokenu?

Nie. JWT to tylko format, czyli ciąg znaków z zakodowanym ładunkiem danych i podpisem. Bezpieczeństwo zależy od tego, jak konkretnie go używasz. Token sesji w httpOnly cookie może być bezpieczniejszy niż JWT w localStorage. JWT ma zalety (jest bezstanowy, zawiera informacje o użytkowniku), ale nie jest magicznym rozwiązaniem.

Czy powinienem używać własnej autentykacji, czy korzystać z Auth.js / Supabase?

Używaj sprawdzonego rozwiązania, chyba że masz bardzo specyficzne wymagania. Własna autentykacja to dziesiątki pułapek bezpieczeństwa i tygodnie pracy. Auth.js, Clerk i Auth0 mają duże zespoły bezpieczeństwa oraz tysiące użytkowników sprawdzających luki.

Co jeśli użytkownik nie ma włączonych ciasteczek?

Wyłączone ciasteczka to rzadki przypadek brzegowy w 2026 roku, ale może się zdarzyć. Awaryjnie pokaż komunikat, że aplikacja wymaga cookies, a nie przenoś tokenów do localStorage. Alternatywą jest osobna aplikacja SPA bez sesji, z JWT trzymanym w pamięci i ponownym logowaniem w każdej karcie, ale doświadczenie użytkownika na tym cierpi.

Co z OAuth / logowaniem przez Google / logowaniem przez Apple?

Przepływ OAuth kończy się na Twoim serwerze tokenem od dostawcy. Od tego momentu używaj standardowego wzorca: wydajesz własny access token i refresh token, a token OAuth trzymasz w bazie. Auth.js obsługuje taki scenariusz bez dodatkowego pisania całego mechanizmu od zera.

Dlaczego przy kilku równoległych żądaniach sesja nagle pada?

Najpewniej kilka żądań dostało 401 jednocześnie i każde uruchomiło własne odświeżenie tokenu. Przy rotacji refresh tokenów pierwsze odświeżenie unieważnia stary token, a drugie (wysłane z tym samym, już zużytym tokenem) wygląda dla serwera jak ponowne użycie skradzionego tokenu i słusznie ubija całą sesję. Rozwiązanie to tryb single-flight: jedna współdzielona obietnica odświeżenia, do której podpinają się wszystkie czekające żądania.

Czy muszę implementować „wyloguj ze wszystkich urządzeń"?

To dobra praktyka, szczególnie dla aplikacji z danymi wrażliwymi. Implementacja to lista aktywnych sesji użytkownika w bazie, token zawiera sessionId, a wylogowanie z danej sesji usuwa wpis. Wylogowanie ze wszystkich urządzeń oznacza usunięcie wszystkich sesji użytkownika.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat React

Czytaj dalej

Zobacz więcej wpisów
Backend dla frontendowca: auth, real-time i integracje

Pierwsza część serii uporządkowała fundamenty: serwer, bazę danych, API i CORS. Teraz przechodzimy do obszarów, które zwykle pojawiają się chwilę później, gdy aplikacja przestaje być prostym CRUD-em: komunikacja w czasie rzeczywistym, webhooki, integracje zewnętrzne oraz autentykacja .

Maciej Sala

Maciej Sala

Founder StriveLab

CSRF, XSS, Injection: 5 luk bezpieczeństwa w Next.js

Przekonanie, że wbudowane w Reacta zabezpieczenia przed XSS w pełni chronią aplikację, to niebezpieczny mit. Współczesny Next.js to nie tylko warstwa prezentacyjna, ale pełnoprawne środowisko serwerowe z Server Actions, Route Handlerami, Server Components i bezpośrednim dostępem do nagłówków czy ciasteczek. Każdy z tych punktów styku staje się potencjalnym wektorem ataku, jeśli traktuje się go z taką samą beztroską jak zwykły komponent UI. Oto pięć najczęstszych luk bezpieczeństwa w architekturze Next.js oraz konkretne sposoby na ich wyeliminowanie.

Maciej Sala

Maciej Sala

Founder StriveLab

RBAC w Next.js: Jak zabezpieczyć role i uprawnienia?

Wyobraź sobie, że dla każdego użytkownika z osobna sprawdzasz, czy wolno mu edytować artykuł. Przy dziesięciu osobach jeszcze ujdzie, przy tysiącu to droga przez mękę. RBAC odwraca pytanie: uprawnienia wiążesz z rolami admin, editor, user , role z ludźmi, a w kodzie pytasz tylko, czy rola ma uprawnienie posts:edit . Jedna reguła zamiast tysiąca wyjątków.

Maciej Sala

Maciej Sala

Founder StriveLab