Autoryzacja i autentykacja — JWT, sessions, OAuth dla frontendowca

Zrozum różnicę między logowaniem a uprawnieniami oraz naucz się dobierać sessions, JWT, cookies i OAuth do realnych scenariuszy w aplikacjach webowych.

Opublikowano

20 sierpnia 2025 12:55

Czytanie

6 min czytania

Aktualizacja

15 kwietnia 2026 11:52

"Zaloguj się przez Google", "Nieprawidłowy token", "Sesja wygasła" — używasz tego codziennie, ale czy rozumiesz, co się za tym kryje?

Autentykacja i autoryzacja to fundament bezpieczeństwa aplikacji. Jako frontend developer implementujesz formularze logowania, zarządzasz tokenami, obsługujesz przekierowania OAuth to standard delegowanego dostępu, który pozwala logować użytkownika przez zewnętrznego dostawcę bez ujawniania hasła.. Warto wiedzieć, jak to działa pod spodem — to jeden z kluczowych elementów backendu dla frontendowca.

Krótka odpowiedź: Autentykacja = weryfikacja tożsamości ("kim jesteś?"). Autoryzacja = sprawdzenie uprawnień ("co możesz robić?"). Do przechowywania stanu zalogowania: sesje dla tradycyjnych aplikacji, JWT, czyli JSON Web Token, to podpisany token używany często do autoryzacji i przekazywania tożsamości użytkownika. dla API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. i stateless serwisów. OAuth dla logowania przez Google/GitHub. Refresh token w httpOnly cookie, access token w pamięci aplikacji.

Szybkie porównanie metod

MetodaMechanizmKiedy wybrać
SessionServer przechowuje stan w bazie/Redis, klient dostaje ID w cookieTradycyjna aplikacja webowa, łatwe unieważnianie sesji
JWTToken z danymi usera podpisany przez serwer, klient go przechowujeAPI, microservices, stateless, komunikacja API-to-API
OAuth 2.0Delegacja dostępu do zewnętrznego providera (Google, GitHub)Social login, integracja z third-party serwisami
PasskeysBiometria / klucz sprzętowy, bez hasłaNowoczesne aplikacje, wysoki poziom bezpieczeństwa
Gdzie trzymać tokenBezpieczeństwoRekomendacja
localStorage❌ Podatne na XSSUnikaj dla tokenów auth
httpOnly cookie✅ JS nie ma dostępuRefresh token
Pamięć aplikacji (zmienna)✅ Nie persystuje po zamknięciuAccess token

Autentykacja vs Autoryzacja

Autentykacja (Authentication)

"Kim jesteś?" — weryfikacja tożsamości.

Code
Użytkownik: "Jestem jan@example.com"
System: "Udowodnij to" (hasło, token, biometria)
Użytkownik: "Oto moje hasło: ****"
System: "OK, jesteś Jan Kowalski"

Autoryzacja (Authorization)

"Co możesz robić?" — sprawdzanie uprawnień.

Code
Jan: "Chcę usunąć użytkownika #123"
System: "Czy Jan ma uprawnienia admina?"
System: "Nie → 403 Forbidden"

Kolejność: Najpierw autentykacja, potem autoryzacja.

Metody autentykacji

1. Hasło (password-based)

Klasyczne logowanie:

Code
// Frontend
const response = await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify({ email, password })
})
const { token } = await response.json()
 
// Backend
app.post('/api/login', async (req, res) => {
  const user = await db.user.findUnique({ where: { email } })
  
  const validPassword = await bcrypt.compare(password, user.passwordHash)
  if (!validPassword) {
    return res.status(401).json({ error: 'Invalid credentials' })
  }
  
  const token = jwt.sign({ userId: user.id }, SECRET, { expiresIn: '15m' })
  res.json({ token })
})

2. OAuth 2.0 (Social login)

"Zaloguj przez Google/GitHub/Facebook":

W praktyce samo logowanie społecznościowe to zwykle OAuth 2.0 rozszerzony o OpenID Connect, żeby aplikacja dostała nie tylko dostęp, ale też wiarygodną informację o tożsamości użytkownika.

Code
1. User klika "Login with Google"
2. Przekierowanie do Google
3. User loguje się w Google
4. Google pyta: "Czy zezwolić app X na dostęp?"
5. User zgadza się
6. Google przekierowuje z powrotem z kodem
7. Backend wymienia kod na token
8. Backend pobiera dane usera z Google

Link logowania wysyłany na email:

Code
1. User wpisuje email
2. Backend generuje jednorazowy token
3. Email z linkiem: app.com/login?token=abc123
4. User klika link
5. Backend weryfikuje token → zalogowany

4. Passkeys / WebAuthn

Nowoczesna metoda z biometrią / kluczem sprzętowym:

Code
// Rejestracja
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: serverChallenge,
    rp: { name: "My App" },
    user: { id: userId, name: email, displayName: name },
    pubKeyCredParams: [{ alg: -7, type: "public-key" }]
  }
})
 
// Logowanie
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: serverChallenge,
    allowCredentials: [{ id: credentialId, type: "public-key" }]
  }
})

Sessions vs Tokens

Dwa główne podejścia do utrzymywania stanu zalogowania:

Sessions (server-side)

Stan przechowywany na serwerze:

Code
┌─────────┐                    ┌─────────┐
│ Browser │                    │ Server  │
└────┬────┘                    └────┬────┘
     │                              │
     │  POST /login                 │
     │  {email, password}           │
     │─────────────────────────────▶│
     │                              │ Tworzę sesję w bazie/Redis
     │                              │ session_id = "abc123"
     │  Set-Cookie: session=abc123  │
     │◀─────────────────────────────│
     │                              │
     │  GET /api/profile            │
     │  Cookie: session=abc123      │
     │─────────────────────────────▶│
     │                              │ Sprawdzam sesję w bazie
     │                              │ Sesja valid → user_id = 1
     │  {user data}                 │
     │◀─────────────────────────────│

Zalety:

  • Łatwe unieważnienie (usuń z bazy)
  • Mniejszy payload (tylko ID)
  • Bardziej bezpieczne (dane na serwerze)

Wady:

  • Wymaga storage (baza/Redis)
  • Przy wielu instancjach wymaga współdzielonego storage
  • Więcej infrastruktury niż przy całkiem stateless API

Tokens (JWT)

Stan reprezentowany po stronie klienta przez token:

Code
┌─────────┐                    ┌─────────┐
│ Browser │                    │ Server  │
└────┬────┘                    └────┬────┘
     │                              │
     │  POST /login                 │
     │  {email, password}           │
     │─────────────────────────────▶│
     │                              │ Tworzę JWT z danymi usera
     │                              │ token = sign({userId: 1})
     │  {token: "eyJ..."}           │
     │◀─────────────────────────────│
     │                              │
     │  GET /api/profile            │
     │  Authorization: Bearer eyJ...│
     │─────────────────────────────▶│
     │                              │ Weryfikuję podpis JWT
     │                              │ Token valid → userId = 1
     │  {user data}                 │
     │◀─────────────────────────────│

Zalety:

  • Stateless (łatwe skalowanie)
  • Brak potrzeby storage
  • Wygodne w komunikacji API-to-API i aplikacjach rozproszonych

Wady:

  • Trudne unieważnienie
  • Większy payload
  • Zły storage po stronie przeglądarki szybko robi z tego problem bezpieczeństwa

JWT — głębsze spojrzenie

Struktura JWT

Code
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIiwiZXhwIjoxNjQwMDAwMDAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

[Header].[Payload].[Signature]

JWT nie szyfruje payloadu. Każdy, kto ma token, może go odczytać. Podpis potwierdza integralność, ale nie ukrywa danych.

Header:

Code
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload:

Code
{
  "userId": 123,
  "role": "admin",
  "exp": 1640000000,
  "iat": 1639900000
}

Signature:

Code
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Generowanie JWT (backend)

Code
const jwt = require('jsonwebtoken')
 
// Tworzenie
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '7d' }
)
 
// Weryfikacja
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET)
  console.log(decoded.userId)  // 123
} catch (err) {
  // Token invalid lub expired
}

Używanie JWT (frontend)

Code
// Po logowaniu — trzymaj access token w pamięci aplikacji
let accessToken = data.accessToken
 
// Przy requestach — dołącz token
const response = await fetch('/api/profile', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
})
 
// Wylogowanie — wyczyść stan klienta i unieważnij refresh token na backendzie
accessToken = null

Access Token + Refresh Token

Problem: JWT z długim czasem życia = ryzyko. JWT z krótkim = częste logowanie.

Rozwiązanie: dwa tokeny:

Code
Access Token:  krótki (15 min), używany do API
Refresh Token: długi (7 dni), używany do odświeżania access tokena
Code
// Gdy access token wygasa:
const response = await fetch('/api/refresh', {
  method: 'POST',
  credentials: 'include', // refresh token siedzi w httpOnly cookie
})
 
const { accessToken } = await response.json()

Przechowywanie tokenów

localStorage

Code
localStorage.setItem('token', token)

Prosty Dostępny dla JS → podatny na XSS Słabe domyślne miejsce dla tokenów auth w aplikacji webowej

Code
// Backend ustawia cookie
res.cookie('token', token, {
  httpOnly: true,  // niedostępny dla JS
  secure: true,    // tylko HTTPS
  sameSite: 'lax'  // ochrona CSRF; 'strict' blokuje cookie po redirect z OAuth
})

Bezpieczniejszy (brak dostępu z JS) Wysyłany automatycznie, więc wymaga ochrony CSRF Więcej uwagi przy integracjach cross-site i subdomainach

Rekomendacja

  • Access token: krótko żyjący token w pamięci aplikacji lub sesja/cookie, jeśli architektura na to pozwala
  • Refresh token: httpOnly cookie
  • Unikaj jako domyślnego wyboru: localStorage dla tokenów auth

OAuth 2.0 — szczegóły

Authorization Code Flow

Dla aplikacji z backendem to najczęstszy i najbezpieczniejszy flow. Dla SPA, czyli Single Page Application, działa bez pełnego przeładowania dokumentu przy każdej nawigacji. i innych public clients stosuje się ten sam mechanizm z PKCE.

Code
┌─────────┐          ┌─────────┐          ┌─────────┐
│ Browser │          │ Backend │          │ Google  │
└────┬────┘          └────┬────┘          └────┬────┘
     │                    │                    │
     │ Click "Login       │                    │
     │ with Google"       │                    │
     │───────────────────▶│                    │
     │                    │                    │
     │ Redirect to Google │                    │
     │◀───────────────────│                    │
     │                    │                    │
     │ Login + Consent    │                    │
     │────────────────────────────────────────▶│
     │                    │                    │
     │ Redirect with code │                    │
     │◀────────────────────────────────────────│
     │                    │                    │
     │ Send code          │                    │
     │───────────────────▶│                    │
     │                    │ Exchange code      │
     │                    │ for token          │
     │                    │───────────────────▶│
     │                    │                    │
     │                    │ Access token       │
     │                    │◀───────────────────│
     │                    │                    │
     │                    │ Get user info      │
     │                    │───────────────────▶│
     │                    │                    │
     │                    │ User data          │
     │                    │◀───────────────────│
     │                    │                    │
     │ Create session/JWT │                    │
     │◀───────────────────│                    │

Implementacja z NextAuth.js

Uwaga: Przykład poniżej dotyczy NextAuth.js v4. NextAuth v5 (Auth.js) używa innej konfiguracji — sprawdź oficjalną dokumentację jeśli instalujesz najnowszą wersję.

Code
// pages/api/auth/[...nextauth].js  (NextAuth v4)
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
 
export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.sub
      return session
    },
  },
})
Code
// Komponent
import { signIn, signOut, useSession } from 'next-auth/react'
 
function LoginButton() {
  const { data: session } = useSession()
  
  if (session) {
    return (
      <>
        Zalogowany jako {session.user.email}
        <button onClick={() => signOut()}>Wyloguj</button>
      </>
    )
  }
  
  return <button onClick={() => signIn('google')}>Zaloguj przez Google</button>
}

Middleware autoryzacji

Backend (Express)

Code
// Middleware autentykacji
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1]
  
  if (!token) {
    return res.status(401).json({ error: 'Token required' })
  }
  
  try {
    const decoded = jwt.verify(token, SECRET)
    req.user = decoded
    next()
  } catch {
    res.status(401).json({ error: 'Invalid token' })
  }
}
 
// Middleware autoryzacji
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' })
    }
    next()
  }
}
 
// Użycie
app.get('/api/profile', authenticate, getProfile)
app.delete('/api/users/:id', authenticate, requireRole('admin'), deleteUser)

Frontend (React)

Code
// Hook sprawdzający auth
function useRequireAuth(redirectUrl = '/login') {
  const { user, loading } = useAuth()
  const router = useRouter()
  
  useEffect(() => {
    if (!loading && !user) {
      router.push(redirectUrl)
    }
  }, [user, loading])
  
  return { user, loading }
}
 
// Protected component
function Dashboard() {
  const { user, loading } = useRequireAuth()
  
  if (loading) return <Spinner />
  
  return <div>Welcome, {user.name}</div>
}

Best practices

Hasła

Code
// ❌ Nigdy
user.password = plainPassword
 
// ✅ Zawsze hashuj
const hash = await bcrypt.hash(plainPassword, 12)
user.passwordHash = hash
 
// Weryfikacja
const valid = await bcrypt.compare(inputPassword, user.passwordHash)

Token expiration

Code
// Access token: krótki
jwt.sign(payload, secret, { expiresIn: '15m' })
 
// Refresh token: dłuższy
jwt.sign(payload, secret, { expiresIn: '7d' })

Logout

Code
// Frontend
localStorage.removeItem('token')
 
// Backend (jeśli refresh tokens w bazie)
await db.refreshToken.delete({ where: { userId } })

Rate limiting

Ograniczanie zapytań to kluczowa ochrona przed atakami brute-force — więcej o implementacji w Next.js w szerokim przewodniku Upstash Redis w Next.js — sesje, cache, rate limiting i liczniki.

Code
const rateLimit = require('express-rate-limit')
 
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minut
  max: 5,  // max 5 prób
  message: 'Too many login attempts'
})
 
app.post('/api/login', loginLimiter, loginHandler)

FAQ

Jaka jest różnica między autentykacją a autoryzacją?

Autentykacja (authentication) odpowiada na pytanie "Kim jesteś?" — weryfikuje tożsamość użytkownika (logowanie hasłem, biometrią, tokenem). Autoryzacja (authorization) odpowiada na pytanie "Co możesz robić?" — sprawdza, czy zaautentykowany użytkownik ma uprawnienia do konkretnej operacji. Kolejność jest zawsze taka sama: najpierw autentykacja, potem autoryzacja.

JWT czy sesje — co wybrać?

Sesje są lepszym wyborem gdy: budujesz tradycyjną aplikację webową, potrzebujesz łatwego unieważniania (wylogowanie natychmiast działa), masz jedną domenę. JWT ma sens gdy: budujesz bezstanowe API, komunikujesz się między serwisami (microservices), masz wiele domen lub mobilne aplikacje. W praktyce wiele aplikacji Next.js używa sesji przez NextAuth/Auth.js, bo unieważnianie JWT bez dodatkowej infrastruktury jest problematyczne.

Czy JWT jest bezpieczny?

JWT jest bezpieczny pod warunkiem poprawnej implementacji: krótki czas życia access tokena (15 min), refresh token w httpOnly cookie (nie localStorage), bezpieczny sekret do podpisywania i weryfikacja podpisu na serwerze. Pamiętaj: JWT nie szyfruje danych — payload jest Base64 encodowany i każdy, kto ma token, może go odczytać. Nie umieszczaj w JWT wrażliwych danych jak hasła czy numery kart.

Gdzie bezpiecznie przechowywać JWT token w przeglądarce?

Access token (krótko żyjący, ~15 min) — trzymaj w pamięci aplikacji (zmienna JavaScript). Refresh token (długo żyjący, ~7 dni) — trzymaj w httpOnly cookie, który nie jest dostępny dla JavaScript. Unikaj localStorage dla tokenów auth — jest podatny na ataki XSS.

Co to jest OAuth 2.0 i jak różni się od OpenID Connect?

OAuth 2.0 to protokół delegacji dostępu — pozwala aplikacji działać w imieniu użytkownika u zewnętrznego providera (np. czytać maile z Gmaila). Samo OAuth 2.0 nie mówi nic o tożsamości. OpenID Connect (OIDC) to warstwa na OAuth 2.0, która dodaje mechanizm weryfikacji tożsamości — aplikacja dostaje id_token z informacjami o użytkowniku. "Zaloguj przez Google" to OAuth 2.0 + OIDC.

Jak zaimplementować logowanie przez Google w Next.js?

Najprostsze podejście to NextAuth.js (v4) lub Auth.js (v5). Wymaga: rejestracji aplikacji w Google Cloud Console, skonfigurowania providera GoogleProvider z clientId i clientSecret, dodania NEXTAUTH_SECRET do zmiennych środowiskowych i ustawienia NEXTAUTH_URL. NextAuth obsługuje sesje, callbacki i ochronę routingu out of the box.

Czym różnią się access token i refresh token?

Access token — krótko żyjący (typowo 15 minut), używany do autoryzacji każdego requestu API. Gdy wygasa, klient musi go odświeżyć. Refresh token — długo żyjący (7 dni lub dłużej), używany wyłącznie do uzyskania nowego access tokena. Taki podział ogranicza ryzyko: skradziony access token wygasa szybko, a refresh token siedzi w bezpiecznym httpOnly cookie.

Podsumowanie

MetodaKiedy używać
SessionsTradycyjne aplikacje, łatwe unieważnianie
JWTAPI, microservices, stateless
OAuthSocial login, third-party integration
PasskeysNowoczesne, bezpieczne logowanie
PrzechowywanieBezpieczeństwoUse case
localStorage❌ XSSNigdy dla tokenów
httpOnly cookie✅ BezpieczneRefresh token
Memory✅ BezpieczneAccess token

Auth to krytyczna część każdej aplikacji. Używaj sprawdzonych bibliotek (NextAuth, Passport.js) zamiast pisać od zera.


Chcesz zobaczyć auth w praktyce? Sprawdź tutorial fullstack z Next.js — implementacja od A do Z.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Anthropic uderza w Figmę i Adobe — oto Claude Design

Anthropic uderza w Figmę i Adobe — oto Claude Design

Anthropic wypuścił właśnie narzędzie AI do tworzenia stron, landing page'ów i prezentacji z promptu. Oto co wiemy o Claude Design i Opus 4.7 — i co to oznacza dla developerów.

Maciej Sala

Maciej Sala

Founder Strivelab

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab