- Published on
Autentizace uživatele pomocí JWT
Jak probíhá autentizace uživatele pro přístup k API pomocí JWT?
JWT (JSON Web Token) je jedním ze způsobů, jak provádět autentizaci na webu. Server, ke kterému se chci připojovat, vytvoří token, který potvrzuje, že je uživatel přihlášen. Tento token se uživateli uloží do prohlížeče a při každém dalším požadavku se posílá zpět na server. Server se podívá na token a ověří, že je pravý a platný. Uživatel se proto nemusí při každém požadavku stále dokola přihlašovat.
Stručný popis celého procesu
Proces popíšu pro jednoduchý případ, kdy uživatel posílá data přes formulář (frontend v Reactu), který komunikuje se serverem přes RESTful API (backend v Expressu s knihovnou bcrypt):
| Frontend - React | Backend - Express + bcrypt |
|---|---|
Uživatel vyplní registrační formulář (přihlašovací jméno + heslo) - tím odešle POST request na API endpoint pro registraci (např. /register) | Knihovnou bcrypt se zahešuje heslo a uloží se do databáze |
Uživatel teď může vyplnit přihlašovací formulář - tím odešle POST request na API endpoint pro přihlášení (např. /login) |
|
| V prohlížeči se JWT uloží a může se od teď používat v autorizační hlavičce pro další requesty, a tím se autentizovat. | Při každém dalším požadavku API middleware nejdříve ověří, zda je JWT platný, než umožní pokračování zpracování požadavku. |
Ukázka implementace
Registrace
i. Frontend
Na endpoint
/registerposílám při registraci POST requestem přihlašovací jméno (v našem případě email) a heslo:const handleRegister = async () => { await fetch("https://server-api.com/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "joe@example.com", password: "heslo123", }), }); };ii. Backend
V Expressu definuju endpoint, na kterém zpracuju request a data uložím do databáze. Heslo před uložením "osolím" a zahešuju (jak funguje solení v bcrypt bude vysvětleno později).
const bcrypt = require("bcrypt"); app.post("/register", async (req, res) => { const { email, password } = req.body; const hashedPassword = await bcrypt.hash(password, 10); const newUser = { id: uuid(), email: email, password: hashedPassword }; // Místo pro uložení nového uživatele s přihlašovacími údaji do db res.status(201).json({ message: "Uživatel úspěšně zaregistrován" }); });Přihlášení uživatele
i. Frontend
Na endpoint
/loginposílám při přihlášení POST requestem přihlašovací jméno (v našem případě email) a heslo. V odpovědi mi přijde JWT, který uložím v prohlížeči.const handleLogin = async () => { const res = await fetch("https://server-api.com/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "joe@example.com", password: "heslo123", }), }); const data = await res.json(); localStorage.setItem("token", data.token); // Uložení JWT };ii. Backend
Podle emailu vyhledá v db uživatele. Porovná zahešované heslo z databáze se zadaným heslem při přihlašování. Jako odpověď vrátí JWT. K vytvářenému JWT se připojí JWT_SECRET pro ověření při zpracování následujících requestů (jak funguje JWT_SECRET bude vysvětleno později).
const jwt = require("jsonwebtoken"); const JWT_SECRET = "tajný-klíč"; app.post("/login", async (req, res) => { const { email, password } = req.body; const user = db.findUserByEmail(email); if (!user) return res.status(400).json({ message: "Uživatel nebyl nalezen" }); const passwordMatch = await bcrypt.compare(password, user.password); if (!passwordMatch) return res.status(403).json({ message: "Chybné heslo" }); const token = jwt.sign({ id: user.id, name: user.name }, JWT_SECRET, { expiresIn: "1h", }); res.json({ token }); // Odešle JWT na frontend });Volání zabezpečených služeb
i. Frontend
Pro přístup k zabezpečeným službám musím v hlavičce requestu použít získaný JWT.
const token = localStorage.getItem("token"); fetch("https://your-api.com/protected", { headers: { Authorization: `Bearer ${token}`, }, }) .then((res) => res.json()) .then((data) => console.log(data));ii. Backend Middleware
Funkce
authenticateTokenověří, jestli je JWT v hlavičce requestu validní. Použiju jí potom jako parametr při definici zabezpečených služeb (viz následující příklad).const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization'] const token = authHeader && authHeader.split(' ')[1] if (!token) return res.status(401).json({ message: 'No token provided' }) jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ message: 'Invalid token' }) req.user = user next() }) }iii. Backend - zabezpečená služba
Funkce
authenticateTokenz předchozího bodu je přidaná jako parametr do specifikace služby:app.get('/protected', authenticateToken, (req, res) => { res.json({ message: `Autentizace proběhla ok` }) })
Jak fungeje solení a hešování v bcrypt?
Sůl je náhodný řetězec, který se přidá k heslu před zahešováním. Kdyby dva uživatelé měli stejné heslo, díky osolení mi vzniknou různé hashe.
Jak vypadá výsledek hešovací funkce bcrypt?
Tento kód const hash = await bcrypt.hash("mypassword", 10); přiřadí do proměnné hash něco takového:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
Hash se skládá z několika částí oddělených symbolem $.
Struktura je: $typ_algoritmu$cost_factor$sůl+hash
V našem případě:
- typ algoritmu je 2b
- cost factor (náklady na vytvoření - viz vysvětlení později) je 10
- sůl je N9qo8uLOickgx2ZMRZoMye (22 znaků)
- samotný hash je IjZAgcfl7p92ldGxad68LJZdL17lhWy (31 znaků)
Když potom zavolám bcrypt.compare("mypassword", "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"), bcrypt si z druhého argumentu vytáhne sůl, cost factor a samotný hash a použije je na vytvoření hashe z hesla, s kterým se uživatel pokouší přihlásit. Pak tyto dva hashe porovná a tím zjistí, jestli je přihlašovací heslo ok.
Co je to cost factor?
Cost factor 10 znamená, že bcrypt vykonná 2^10 opakování hashovacího procesu. Na začátku spojí sůl a heslo do jednoho řetězce a pak na něj aplikuje 2^10 krát proces, který je označován jako Expensive Key Schedule. Zvyšuje tím tak časovou náročnost na zahešování hesla, ale i na jeho zpětné prolomení.
Struktura tokenu
JWT má tři části oddělené tečkami: header.payload.signature
- header: obsahuje použitý algoritmus (v našem případě HS256) a typ tokenu (JWT)
- payload: může obsahovat různá data, jako např. iduživatele, expirace tokenu apod.
- signature: samotný podpis
Jak funguje podepisování tokenu?
Při vytváření JWT se token "podepisuje", čímž se zajišťuje jeho pravost. V zásadě jsou dva typy algoritmů, které můžeme k podepsání použít:
- HS256 (algoritmus typu HMAC - symetrický algoritmus) Pro podepisování i ověření se používá jeden společný klíč (náš JWT_SECRET). Je to soukromý klíč (musí být udržen v tajnosti).
- RS256 nebo ES256 (asymetrické algoritmy) Používá dvojici soukromý a veřejný klíč. Soukromý je určený pro podepisování tokenu, veřejný pro verifikaci (může být sdílen). U našeho případu by byl
JWT_SECRETnahrazen soukromým klíčem při podepisování. Ověřování by probíhalo pomocí veřejného klíče vjwt.verify. Tento způsob se častěji používá v prostředí s více službami (např. mikroservisy), kde potřebujeme bezpečně ověřovat tokeny mezi různými servery.
Podepsání tokenu step-by-step
Zavolám funkci jwt.sign(payload, secret, options) která vytvoří JWT v těchto krocích:
Vytvoření hlavičky
Vytvoří JSON objekt jako např.:
{
"alg": "HS256",
"typ": "JWT"
}
Zakódování hlavičky a payloadu
Payload může vypadat např. takto:
{
"userId": 123,
"exp": 1712345678
}
Pro kódování je použité base64, které je URL-safe.
Vygenerování podpisu Kombinací hešovací funkce s tajným šifrovacím klíčem (v našem případě JWT_SECRET) se vytvoří podpis (signature):
signature = HMAC_SHA256(base64Url(header) + "." + base64Url(payload), secret)Spojení všech tří částí do JWT
header.payload.signature
Ověření tokenu step-by-step
Zavolám funkci jwt.verify(token, secret)
- Rozdělení tokenu
Token je rozdělený na header, payload a signature
- Přegenerování podpisu
Z hlavičky a payloadu se znuvu vypočítá podpis (signature) s použitím šifrovacího klíče (parametr secret ve funkci verify - v našem případě JWT_SECRET)
- Porovnání podpisů (signature)
Pokud vypočítaný podpis odpovídá podpisu v tokenu, tak je poslaný token ok.
- Ověření časové platnosti
Pokud má token časovou platnost, tak se ověří, jestli nevypršela.
- Return payload
Pokud je vše ok, funkce vrací dekódovaný payload z tokenu.

