W poprzednich wpisach poznaliśmy React Query i zaawansowane wzorce pobierania danych. Ale każda prawdziwa aplikacja potrzebuje autentykacji (uwierzytelniania – weryfikacji tożsamości użytkownika) i autoryzacji (nadawania uprawnień – sprawdzania czy użytkownik ma dostęp do zasobu).
🔍 Autentykacja vs Autoryzacja - różnica:
Autentykacja
Autoryzacja
Pytanie
"KIM jesteś?"
"CO możesz robić?"
Weryfikuje
Tożsamość
Uprawnienia
Przykład
Login + hasło
Admin może usuwać, User tylko czytać
Analogia
Dowód osobisty 🪪
Karta dostępu 🔑
W skrócie: Autentykacja = "zaloguj się", Autoryzacja = "sprawdź czy możesz to zrobić"
W tym wpisie zbudujemy kompletny system autentykacji od zera! Nauczymy się jak działają JWT tokens (tokeny JSON Web Token – zaszyfrowane dane użytkownika), jak zaimplementować login i rejestrację, gdzie przechowywać tokeny (localStorage vs cookies vs httpOnly), jak chronić protected routes (chronione ścieżki – strony dostępne tylko dla zalogowanych), jak działa refresh token mechanism (mechanizm odświeżania tokenów – automatyczne przedłużanie sesji), Axios interceptors (przechwytywacze żądań – automatyczne dodawanie tokenów do requestów) i wylogowanie.
To będzie bardzo praktyczny wpis z pełnym działającym kodem! Po nim będziesz potrafił zaimplementować bezpieczną autentykację w każdej aplikacji React.
JWT Tokens – jak działają?
Czym jest JWT?
JWT (JSON Web Token) to zakodowany string zawierający informacje o użytkowniku. Token składa się z trzech części oddzielonych kropkami:
🔍 JWT - co to jest w prostych słowach?
JWT = cyfrowa przepustka 🎫
Wyobraź sobie koncert:
Login = kupisz bilet przy wejściu
JWT = dostajesz opaskę na rękę z kodem QR
Protected routes = różne strefy (VIP, backstage)
Weryfikacja = ochroniarz skanuje opaskę → puszcza lub nie
Opaska (JWT) zawiera: Twoje imię, jaką masz strefę (admin/user), kiedy wygasa
Serwer tworzy JWT → pakuje dane użytkownika w token
Zwraca token → frontend zapisuje token
Każdy request → frontend wysyła token w nagłówku Authorization: Bearer <token>
Serwer weryfikuje token → sprawdza podpis i czy nie wygasł
Zwraca dane → jeśli token OK, użytkownik dostaje dostęp
🌟 Przykład: Dekodowanie JWT
JWT NIE jest zaszyfrowany - jest zakodowany Base64! Możesz go zdekodować:
// Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywibmFtZSI6IkphbiJ9.abc123
// Dekoduj w konsoli:
const payload = JSON.parse(atob("eyJ1c2VySWQiOjEyMywibmFtZSI6IkphbiJ9"));
console.log(payload);
// { userId: 123, name: "Jan" }
// Albo na https://jwt.io
// Wklej token → widać payload!
// ⚠️ WAŻNE:
// - JWT NIE jest tajny! Każdy może go zdekodować!
// - NIE przechowuj haseł, kart kredytowych w JWT!
// - PODPIS chroni przed MODYFIKACJĄ, nie przed ODCZYTANIEM!
Access Token vs Refresh Token
🔍 Dlaczego dwa tokeny?
Problem z jednym tokenem:
❌ Krótki (15 min) → użytkownik musi się logować co 15 min 😤
❌ Długi (7 dni) → jeśli ukradną, mają dostęp przez 7 dni! 😱
Rozwiązanie - dwa tokeny:
Access Token (krótki):
- Czas życia: 15 minut
- Używany: przy każdym request
- Przechowywany: pamięć / localStorage (mniej bezpieczne OK)
- Jeśli ukradną: szkoda tylko 15 min ✅
Refresh Token (długi):
- Czas życia: 7 dni
- Używany: tylko do odświeżenia Access Token
- Przechowywany: httpOnly cookie (bardzo bezpieczne!)
- Jeśli ukradną: trudniej, bo httpOnly
Flow:
Access Token wygasł (po 15 min)
Frontend: używa Refresh Token → POST /refresh
Serwer: zwraca NOWY Access Token
Frontend: kontynuuje (użytkownik nie zauważa!)
Access Token – krótkotrwały (15 min), używany do każdego requesta
Refresh Token – długotrwały (7 dni), używany do odświeżenia Access Token
Dlaczego dwa tokeny?
Jeśli ktoś ukradnie Access Token, szkoda jest minimalna (wygasa za 15 min)
Refresh Token jest przechowywany bezpieczniej (httpOnly cookie)
Refresh Token pozwala użytkownikowi pozostać zalogowanym bez ciągłego logowania
Przechowywanie tokenów
Opcja 1: localStorage (NIE zalecane dla produkcji!)
⚠️ localStorage - wygodne, ale niebezpieczne!
Atak XSS (Cross-Site Scripting):
// Scenariusz ataku:
// 1. Hacker wstrzykuje złośliwy skrypt na stronę
// 4. Hacker ma Twój token!
// 5. Może wysyłać requesty jako Ty! 💀
Jeśli hacker wstrzyknie złośliwy kod, może ukraść token
Nie zalecane dla wrażliwych danych
Kiedy można użyć:
Aplikacje demo/nauka
Mniej wrażliwe dane
Projekty wewnętrzne
Opcja 2: httpOnly cookies (ZALECANE!)
🔍 httpOnly cookie - jak działa ochrona?
httpOnly = JavaScript NIE MA DOSTĘPU do cookie!
// Serwer ustawia cookie:
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict
// W przeglądarce cookie ISTNIEJE, ale:
document.cookie // ← NIE WIDAĆ refreshToken!
// Próba kradzieży:
const token = document.cookie; // ← refreshToken NIEWIDOCZNY!
// Cookie jest automatycznie wysyłany przy każdym request:
fetch('/api/refresh') // ← Cookie idzie automatycznie!
// Ale JavaScript nie może go odczytać = BEZPIECZNE! 🛡️
Dodatkowe zabezpieczenia:
Secure - tylko HTTPS (nie HTTP)
SameSite=Strict - tylko z tej samej domeny
// ✅ Serwer ustawia cookie w odpowiedzi HTTP
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict
💪 Ćwiczenie 1: Przechowywanie tokenów
Zaprojektuj strategię przechowywania dla aplikacji bankowej:
Access Token - gdzie przechowasz? Dlaczego?
Refresh Token - gdzie przechowasz? Dlaczego?
Jakie zabezpieczenia dodasz?
Co zrobisz jeśli Access Token wygaśnie?
Co zrobisz jeśli Refresh Token wygaśnie?
Wskazówka: Bezpieczeństwo > wygoda!
Zalety:
JavaScript NIE ma dostępu → ochrona przed XSS
Automatycznie wysyłane z każdym requestem
Bezpieczniejsze dla Refresh Token
Wady:
Wymaga konfiguracji CORS na serwerze
Podatne na CSRF (Cross-Site Request Forgery) → rozwiązanie: SameSite flag
🔍 CSRF - co to za atak?
CSRF (Cross-Site Request Forgery) = wymuszony request z innej strony
// Scenariusz ataku CSRF:
// 1. Jesteś zalogowany na bank.com (masz cookie!)
// 2. Odwiedzasz złośliwą stronę zlodzieje.com
// 3. Na zlodzieje.com jest ukryty kod:
// 4. Przeglądarka automatycznie wysyła cookie do bank.com!
// 5. Bank myśli że TO TY wysyłasz request! 💸
// 6. Przelew się wykonuje!
// Ochrona - SameSite=Strict:
Set-Cookie: token=abc; SameSite=Strict
// Teraz cookie NIE JEST wysyłane z innej domeny!
// Request z zlodzieje.com → cookie NIE IDZIE → BEZPIECZNE! 🛡️
Opcja 3: Hybrid (najlepsza praktyka!)
🔍 Hybrid approach - best of both worlds!
Access Token w pamięci (zmiennej):
✅ Znika po refresh strony (minimalne ryzyko kradzieży)
✅ Krótkotrwały (15 min) - nawet jeśli ukradną, szkoda mała
❌ Użytkownik musi się "relogować" po refresh (ale automatycznie!)
Refresh Token w httpOnly cookie:
✅ Bezpieczny (JavaScript nie ma dostępu)
✅ Długotrwały (7 dni) - użytkownik pozostaje zalogowany
✅ Automatycznie wysyłany przy /refresh
Flow po refresh strony:
Access Token ZNIKNĄŁ (był w zmiennej)
Refresh Token ISTNIEJE (httpOnly cookie)
Przy montowaniu: POST /refresh
Serwer: sprawdza Refresh Token z cookie
Zwraca NOWY Access Token
Użytkownik ZALOGOWANY! Nawet nie zauważył! ✨
// ✅ Access Token w memory (zmiennej)
let accessToken: string | null = null;
// ✅ Refresh Token w httpOnly cookie (ustawiane przez serwer)
Set-Cookie: refreshToken=xyz; HttpOnly; Secure; SameSite=Strict
Dlaczego najlepsze?
Access Token w pamięci → znika po odświeżeniu (minimalne ryzyko)
Refresh Token w httpOnly → bezpieczny, długotrwały
Najlepsza równowaga bezpieczeństwo/wygoda
Implementacja AuthContext
Stwórzmy context do zarządzania autentykacją w całej aplikacji.
🔍 AuthContext - po co?
Bez AuthContext:
❌ Każdy komponent osobno sprawdza czy zalogowany
❌ Login/logout w wielu miejscach (duplikacja kodu)
❌ User data przechodzą przez props (prop drilling)
Z AuthContext:
✅ JEDEN stan autentykacji dla całej aplikacji
✅ Login/logout z każdego miejsca: useAuth()
✅ user, isAuthenticated dostępne wszędzie
✅ Automatyczne sprawdzanie przy starcie (checkAuth)
// contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { apiClient } from '../api/client';
interface User {
id: number;
email: string;
name: string;
role: string;
}
interface AuthContextType {
user: User | null;
accessToken: string | null;
login: (email: string, password: string) => Promise<void>;
register: (name: string, email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
isAuthenticated: boolean;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Sprawdź czy użytkownik jest zalogowany przy montowaniu
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
try {
// Spróbuj odświeżyć token (używa refresh token z httpOnly cookie)
const response = await apiClient.post('/auth/refresh');
const { accessToken, user } = response.data;
setAccessToken(accessToken);
setUser(user);
} catch (error) {
// Brak ważnego refresh token - użytkownik niezalogowany
setAccessToken(null);
setUser(null);
} finally {
setIsLoading(false);
}
}
async function login(email: string, password: string) {
const response = await apiClient.post('/auth/login', { email, password });
const { accessToken, user } = response.data;
// refreshToken jest ustawiony automatycznie w httpOnly cookie przez serwer
setAccessToken(accessToken);
setUser(user);
}
async function register(name: string, email: string, password: string) {
const response = await apiClient.post('/auth/register', { name, email, password });
const { accessToken, user } = response.data;
setAccessToken(accessToken);
setUser(user);
}
async function logout() {
try {
// Wyloguj na serwerze (usuń refresh token)
await apiClient.post('/auth/logout');
} finally {
// Wyczyść stan lokalny (nawet jeśli request się nie powiódł)
setAccessToken(null);
setUser(null);
}
}
const value = {
user,
accessToken,
login,
register,
logout,
isAuthenticated: !!user,
isLoading,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Hook do używania AuthContext
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
🔍 checkAuth() - dlaczego przy montowaniu?
Scenariusz: Użytkownik odświeża stronę
Access Token ZNIKNĄŁ (był w zmiennej)
user = null, accessToken = null
useEffect wywołuje checkAuth()
checkAuth: POST /refresh (wysyła Refresh Token z cookie)
Serwer: sprawdza Refresh Token, zwraca nowy Access Token
setAccessToken + setUser
Użytkownik NADAL ZALOGOWANY! 🎉
Bez checkAuth: użytkownik byłby wylogowany po każdym refresh! 😱
Interceptors (przechwytywacze) automatycznie modyfikują każdy request/response.
🔍 Interceptors - jak działają?
Request Interceptor = "przed wysłaniem"
// Każdy request przechodzi przez interceptor:
fetch('/api/users')
↓
Request Interceptor
↓ Dodaje: Authorization: Bearer abc123
↓
Wysyłka do API
Response Interceptor = "po otrzymaniu"
API zwraca 401 Unauthorized
↓
Response Interceptor
↓ Sprawdza: czy 401?
↓ Tak! Wywołuje /refresh
↓ Dostaje nowy token
↓ Ponawia oryginalny request
↓
Zwraca dane (użytkownik nie zauważył problemu!)
// api/client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api',
withCredentials: true, // Wysyłaj cookies (refresh token)
});
let accessToken: string | null = null;
// Funkcja do ustawiania tokenu (wywoływana z AuthContext)
export function setAccessToken(token: string | null) {
accessToken = token;
}
// Request Interceptor - dodaj token do każdego requesta
apiClient.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response Interceptor - obsługa błędów i refresh token
apiClient.interceptors.response.use(
(response) => response, // Jeśli OK, zwróć response
async (error) => {
const originalRequest = error.config;
// Jeśli 401 (Unauthorized) i to nie retry (powtórka)
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // Oznacz jako retry
try {
// Spróbuj odświeżyć token
const response = await axios.post(
`${apiClient.defaults.baseURL}/auth/refresh`,
{},
{ withCredentials: true } // Wyślij refresh token z cookie
);
const newAccessToken = response.data.accessToken;
setAccessToken(newAccessToken);
// Ponów oryginalny request z nowym tokenem
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
// Refresh się nie powiódł - wyloguj użytkownika
setAccessToken(null);
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
🔍 Response Interceptor - flow szczegółowo:
// Użytkownik: GET /api/users (Access Token wygasł!)
1. Request idzie do API
↓
2. API zwraca 401 Unauthorized (token wygasł)
↓
3. Response Interceptor łapie błąd
↓ Sprawdza: error.status === 401?
↓ Tak!
↓ Sprawdza: _retry flag?
↓ Nie ma → to pierwsze podejście
↓
4. Ustawia originalRequest._retry = true
↓
5. POST /auth/refresh (wysyła Refresh Token z cookie)
↓
6. Serwer zwraca nowy Access Token
↓
7. setAccessToken(newToken)
↓
8. Ponawia oryginalny request:
apiClient(originalRequest) ← ale teraz z nowym tokenem!
↓
9. Tym razem 200 OK!
↓
10. Zwraca dane do użytkownika
// Użytkownik NIE ZAUWAŻYŁ że token wygasł! ✨
// Wszystko działa płynnie!
Co tu się dzieje?
Request interceptor – dodaje Authorization: Bearer <token> do każdego requesta
Response interceptor – gdy dostanie 401:
Próbuje odświeżyć token używając refresh token
Jeśli refresh OK → ponawia oryginalny request z nowym tokenem
// components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string; // Opcjonalnie: wymagana rola
}
function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { isAuthenticated, isLoading, user } = useAuth();
const location = useLocation();
// Podczas ładowania pokaż loader
if (isLoading) {
return (
<div className="loading-screen">
<div className="spinner">Sprawdzanie autoryzacji...</div>
</div>
);
}
// Jeśli niezalogowany, przekieruj do login
if (!isAuthenticated) {
// Zapisz gdzie użytkownik chciał iść (dla przekierowania po loginie)
return <Navigate to="/login" state={{ from: location }} replace />;
}
// Jeśli wymagana rola i użytkownik jej nie ma
if (requiredRole && user?.role !== requiredRole) {
return (
<div className="access-denied">
<h1>Brak dostępu</h1>
<p>Nie masz uprawnień do tej strony.</p>
</div>
);
}
// Wszystko OK - pokaż chronioną stronę
return <>{children}</>;
}
export default ProtectedRoute;
🔍 state={{ from: location }} - po co?
Problem bez state:
// Użytkownik próbuje wejść na /profile
→ Niezalogowany → redirect /login
→ Loguje się
→ Przekierowanie do... /dashboard (domyślnie)
// Ale chciał na /profile! 😕
Rozwiązanie z state:
// Użytkownik próbuje wejść na /profile
→ Niezalogowany →
→ Loguje się
→ LoginPage czyta: location.state?.from?.pathname
→ navigate(from) → ląduje na /profile! ✅
// Dokładnie tam gdzie chciał!
Użycie w routingu
🔍 Routing strategy - publiczne vs chronione:
Publiczne routes (login, register):
Dostępne dla wszystkich
Ale jeśli ZALOGOWANY → redirect do dashboard (po co login gdy już zalogowany?)
Zapisz gdzie użytkownik chciał iść przed logowaniem i przekieruj tam po zalogowaniu.
🌟 Przykład: UX z przekierowaniem
// Scenariusz bez przekierowania (ZŁY UX):
1. Użytkownik klika link do /profile
2. Nie zalogowany → /login
3. Loguje się
4. Ląduje na /dashboard
5. "Gdzie mój profil?" 😕
6. Musi ręcznie przejść do /profile
// Scenariusz z przekierowaniem (DOBRY UX):
1. Użytkownik klika link do /profile
2. Nie zalogowany → /login (z state: { from: '/profile' })
3. Loguje się
4. Automatycznie ląduje na /profile! ✅
5. "Wow, to działa!" 🎉
// pages/LoginPage.tsx (poprawiona wersja)
import { useLocation } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
// Pobierz gdzie użytkownik chciał iść (z state przekierowania)
const from = location.state?.from?.pathname || '/dashboard';
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsLoading(true);
try {
await login(email, password);
navigate(from, { replace: true }); // Przekieruj gdzie chciał iść
} catch (err: any) {
setError(err.response?.data?.message || 'Błąd logowania');
} finally {
setIsLoading(false);
}
}
// ... reszta kodu
}
Scenariusz:
Użytkownik próbuje wejść na /profile
Nie jest zalogowany → przekierowanie do /login z state: { from: '/profile' }
Loguje się
Automatyczne przekierowanie do /profile (tam gdzie chciał!)
Navbar z informacją o użytkowniku
🔍 Navbar - dynamiczny UI dla zalogowanych vs niezalogowanych:
// components/AdminButton.tsx
import { usePermissions } from '../hooks/usePermissions';
function Dashboard() {
const { isAdmin, hasPermission } = usePermissions();
return (
<div>
<h1>Dashboard</h1>
{/* Pokaż tylko adminowi */}
{isAdmin && (
<button onClick={handleDeleteAllUsers}>
Usuń wszystkich użytkowników
</button>
)}
{/* Pokaż gdy ma permission */}
{hasPermission('users.edit') && (
<button onClick={handleEditUser}>
Edytuj użytkownika
</button>
)}
{/* Pokaż wszystkim */}
<button onClick={handleSave}>Zapisz</button>
</div>
);
}
💪 Ćwiczenie 3: Role-based Dashboard
Stwórz Dashboard z różnymi widokami dla ról:
Admin widzi:
Wszystkich użytkowników (lista z opcją usuwania)
Statystyki systemu
Logi systemowe
Moderator widzi:
Zgłoszenia użytkowników
Statystyki moderacji
Może banować użytkowników
User widzi:
Swój profil
Swoje posty
Reklamy (admini ich nie widzą!)
Użyj usePermissions() + conditional rendering!
Security Best Practices - najlepsze praktyki bezpieczeństwa
1. HTTPS only
// ✅ Zawsze używaj HTTPS w produkcji
const apiClient = axios.create({
baseURL: 'https://api.example.com', // HTTPS!
withCredentials: true,
});
2. HttpOnly cookies dla refresh token
// ✅ Serwer ustawia httpOnly
Response.Cookies.Append("refreshToken", token, new CookieOptions
{
HttpOnly = true, // JavaScript nie ma dostępu
Secure = true, // Tylko przez HTTPS
SameSite = SameSiteMode.Strict, // Ochrona przed CSRF
Expires = DateTimeOffset.UtcNow.AddDays(7)
});
3. Access token w memory, NIE w localStorage
// ✅ Token w pamięci (zmiennej)
let accessToken: string | null = null;
// ❌ NIE robić tego w produkcji
localStorage.setItem('accessToken', token);
4. Krótki czas życia access token
// ✅ Access token wygasa szybko (15 min)
var accessToken = new JwtSecurityToken(
expires: DateTime.UtcNow.AddMinutes(15),
// ...
);
// Refresh token żyje dłużej (7 dni)
var refreshToken = GenerateRefreshToken(user, expiresInDays: 7);
5. Token rotation
🔍 Token Rotation - dlaczego ważne?
Bez rotation:
Refresh Token = ten sam przez 7 dni
Jeśli ukradną → mają dostęp przez 7 dni! 😱
Z rotation:
Przy każdym /refresh → NOWY Refresh Token
Stary token → invalidate (nieważny)
Jeśli ukradną stary → nie zadziała (już invalidated)!
Dodatkowo: wykrywamy atak (ktoś używa starego tokenu)
// ✅ Nowy refresh token przy każdym odświeżeniu
[HttpPost("refresh")]
public async Task RefreshToken()
{
var oldRefreshToken = Request.Cookies["refreshToken"];
// Invalidate stary token
await _authService.RevokeRefreshToken(oldRefreshToken);
// Wygeneruj NOWY refresh token
var newRefreshToken = _authService.GenerateRefreshToken(user);
Response.Cookies.Append("refreshToken", newRefreshToken, ...);
return Ok(new { accessToken = newAccessToken });
}
Kompletny flow - krok po kroku
🔍 Cały flow autentykacji - timeline:
🏁 START - użytkownik otwiera aplikację
1. AuthProvider montuje się
→ useEffect wywołuje checkAuth()
→ POST /auth/refresh (próba odświeżenia)
Scenariusz A: Ma ważny Refresh Token
→ Serwer: zwraca Access Token + user
→ setState: accessToken, user
→ Użytkownik ZALOGOWANY! ✅
Scenariusz B: Brak/nieważny Refresh Token
→ Serwer: 401
→ setState: null, null
→ Użytkownik NIEZALOGOWANY ❌
2. Routing sprawdza isAuthenticated
→ Zalogowany? → /dashboard
→ Niezalogowany? → /login
3. Użytkownik loguje się
→ POST /auth/login { email, password }
→ Serwer: Access Token + Refresh Token (cookie)
→ setState: accessToken, user
→ navigate('/dashboard')
4. Użytkownik klika "Pokaż zamówienia"
→ GET /api/orders
→ Interceptor: dodaje "Authorization: Bearer ..."
→ Serwer: 200 OK + dane
→ Pokazuje zamówienia
5. 15 minut później... Access Token wygasł
→ GET /api/orders
→ Serwer: 401 Unauthorized
→ Response Interceptor:
→ POST /auth/refresh (Refresh Token w cookie)
→ Serwer: nowy Access Token
→ Ponawia GET /api/orders
→ 200 OK + dane
→ Użytkownik NIC nie zauważył! ✨
6. Użytkownik klika "Wyloguj"
→ POST /auth/logout
→ Serwer: usuwa Refresh Token
→ setState: null, null
→ navigate('/login')
🏁 KONIEC
1. Użytkownik otwiera aplikację
// AuthProvider sprawdza czy jest zalogowany
useEffect(() => {
checkAuth(); // Próbuje odświeżyć token
}, []);
2. Login
Frontend: POST /auth/login { email, password }
Backend:
- Weryfikuje dane
- Generuje accessToken (15 min)
- Generuje refreshToken (7 dni)
- Ustawia refreshToken w httpOnly cookie
- Zwraca { accessToken, user }
Frontend:
- Zapisuje accessToken w memory
- Zapisuje user w state
- Przekierowuje do /dashboard
3. Request do chronionego endpoint
Frontend: GET /api/orders
Axios interceptor: Dodaje header "Authorization: Bearer "
Backend: Weryfikuje token, zwraca dane
Frontend: Wyświetla dane
4. Access token wygasa (po 15 min)
Frontend: GET /api/orders
Backend: 401 Unauthorized (token wygasł)
Axios interceptor:
- Przechwytuje 401
- POST /auth/refresh (z refreshToken w cookie)
Backend:
- Sprawdza refreshToken
- Generuje nowy accessToken
- (Opcjonalnie) Nowy refreshToken
- Zwraca { accessToken }
- Zapisuje nowy accessToken
- Ponawia oryginalny request GET /api/orders
Backend: Zwraca dane
Frontend: Wyświetla dane (użytkownik NIC nie zauważył!)
5. Wylogowanie
Frontend: POST /auth/logout
Backend:
- Invalidate refreshToken
- Usuwa cookie
Frontend:
- Czyści accessToken (null)
- Czyści user (null)
- Przekierowuje do /login
Podsumowanie
Zbudowaliśmy kompletny system autentykacji! Nauczyliśmy się:
✅ JWT Tokens – jak działają, access vs refresh
✅ Przechowywanie tokenów – memory + httpOnly cookies (hybrid)
✅ Security best practices – 5 zasad bezpieczeństwa
✅ Kompletny flow – cały proces krok po kroku
Twoja aplikacja jest teraz bezpieczna! Użytkownicy mogą się logować, pozostawać zalogowani, a tokeny są automatycznie odświeżane w tle.
W kolejnym wpisie (ostatnim w serii!) zbudujemy kompletną Full Stack aplikację łączącą wszystko czego się nauczyliśmy – React + TypeScript frontend z .NET Core API backend, CRUD operations, autentykacja, React Query, deployed na żywo!