JWT eli JSON Web Token on yleisin tapa todentaa käyttäjiä moderneissa Node.js-rajapinnoissa. Token kulkee jokaisen pyynnön mukana, palvelin tarkistaa allekirjoituksen eikä istuntoja tarvitse tallentaa tietokantaan. Tämä opas rakentaa täyden kirjautumisjärjestelmän Express 5:llä ja jsonwebtoken-kirjastolla: rekisteröinti, kirjautuminen, suojatut reitit, refresh-tokenit ja niiden kierto. Käytät versioita, jotka ovat tuotantokelpoisia kesäkuussa 2026, ja vältät ne tietoturva-aukot, jotka kaatavat suurimman osan aloittelijoiden JWT-toteutuksista.
Työ vie noin 40 minuuttia. Lopputuloksena on toimiva projekti, jonka voit testata curl-komennoilla ja viedä sellaisenaan palvelimelle. Päivitetty 15. kesäkuuta 2026.
Mitä JWT on ja miksi sitä kannattaa käyttää
JWT on standardoitu (RFC 7519) tapa siirtää väitteitä eli claimeja kahden osapuolen välillä allekirjoitetussa muodossa. Kun käyttäjä kirjautuu sisään, palvelin luo tokenin, allekirjoittaa sen salaisella avaimella ja lähettää sen selaimelle. Selain liittää tokenin jokaiseen seuraavaan pyyntöön. Palvelin tarkistaa allekirjoituksen ja luottaa tokenin sisältöön ilman tietokantahakua.
Tämä tilattomuus on JWT:n suurin etu perinteisiin istuntoihin verrattuna. Palvelin ei pidä yllä istuntotaulua, joten sovellus skaalautuu vaakasuunnassa ilman jaettua istuntovarastoa kuten Redisiä. Sama token toimii usealla palvelimella, kunhan ne jakavat saman allekirjoitusavaimen. Mikropalveluissa ja mobiilisovelluksissa tämä on ratkaiseva ero. Jos haluat verrata lähestymistapaa palvelinpuolen istuntoihin, lue erillinen oppaamme turvallisista sessioista Node.js:ssä.
Tilattomuudella on hintansa. Koska palvelin ei seuraa tokeneita, vanhentunutta tai varastettua tokenia ei voi suoraan kumota ennen sen vanhentumisaikaa. Tähän ongelmaan vastaa kahden tokenin malli (lyhytikäinen access-token ja pidempi refresh-token), jonka rakennat vaiheessa 8.
JWT vai palvelinistunto: milloin kumpaakin
JWT ei ole automaattisesti parempi kuin perinteinen istunto, vaikka markkinointi joskus näin antaa ymmärtää. Valinta riippuu arkkitehtuurista. Tämä taulukko auttaa päättämään.
| Tilanne | JWT | Palvelinistunto |
|---|---|---|
| Useita backend-palvelimia | Skaalautuu helposti, ei jaettua varastoa | Vaatii jaetun istuntovaraston (Redis) |
| Mobiilisovellus tai SPA | Luonteva valinta, token kulkee otsakkeessa | Eväste-pohjainen, kankeampi |
| Välitön uloskirjautuminen kaikilta laitteilta | Vaatii lisätyötä (musta lista) | Sisäänrakennettu, poista istunto |
| Mikropalvelut, eri tiimit | Julkinen avain tarkistaa, ei jaettua tilaa | Hankala, vaatii keskitetyn istuntopalvelun |
| Yksi monoliittinen sovellus | Toimii, mutta ei pakollinen | Yksinkertainen ja riittävä |
Nyrkkisääntö: valitse JWT, kun sinulla on useita palveluita tai mobiiliasiakkaita, jotka kuluttavat samaa rajapintaa. Valitse palvelinistunto, kun rakennat yhtä perinteistä verkkosovellusta ja haluat yksinkertaisen uloskirjautumisen. Tämä opas keskittyy JWT:hen, koska se on yleisin valinta moderneissa API-pohjaisissa sovelluksissa, mutta on hyvä tuntea molemmat. Käytännössä monet tuotantojärjestelmät yhdistävät molemmat: JWT rajapintakutsuihin ja palvelinistunto hallintapaneeliin.
JWT:n rakenne: header, payload ja allekirjoitus
JWT koostuu kolmesta osasta, jotka erotetaan pisteellä: header.payload.signature. Jokainen osa on Base64URL-koodattu, mikä eroaa tavallisesta Base64:stä siten, että se on turvallinen URL-osoitteissa. Header kertoo tokenin tyypin ja allekirjoitusalgoritmin, esimerkiksi {"alg":"HS256","typ":"JWT"}. Payload sisältää claimit kuten sub (käyttäjän tunniste), iat (luontiaika) ja exp (vanhentumisaika). Allekirjoitus lasketaan headerista ja payloadista salaisella avaimella.
Tärkeä ja usein väärin ymmärretty seikka: payload on vain koodattu, ei salattu. Kuka tahansa voi purkaa Base64URL-koodauksen ja lukea sisällön. Älä koskaan laita tokeniin salasanoja, henkilötunnuksia tai muuta arkaluontoista tietoa. Allekirjoitus takaa eheyden (sisältöä ei ole muutettu), ei luottamuksellisuutta. Jos tarvitset salausta, käytä JWE-muotoa tavallisen JWS:n sijaan.
Esitiedot ja vaaditut versiot
Tarvitset perustiedot JavaScriptistä ja komentorivistä sekä asennettuna Node.js:n. Alla olevat versiot ovat tämän oppaan testiympäristö kesäkuussa 2026. Käytä Active LTS -versiota, älä koskaan pelkkää Current-haaraa tuotannossa.
| Komponentti | Versio | Rooli projektissa |
|---|---|---|
| Node.js | 24.16.0 LTS (Krypton) | Suoritusympäristö, julkaistu 21.5.2026 |
| npm | 11.x (tulee Noden mukana) | Pakettienhallinta |
| express | 5.2.1 | HTTP-palvelin ja reititys |
| jsonwebtoken | 9.0.3 | Tokenien luonti ja todennus |
| bcrypt | 6.0.0 | Salasanojen tiivistäminen |
| cookie-parser | 1.4.7 | Evästeiden lukeminen |
| dotenv | 17.4.2 | Ympäristömuuttujien lataus |
Jos käytät vanhempaa Node.js 22 LTS -versiota, se saa tietoturvapäivityksiä huhtikuuhun 2027 asti ja kelpaa myös. Voit tarkistaa Noden elinkaaren virallisesta julkaisutaulukosta. Express 5 on uusin pääversio, ja se vaatii Node.js 18:aa tai uudempaa.
Vaihe 1: Projektin alustus ja riippuvuuksien asennus
Luo projektikansio ja alusta npm-projekti. Asetamme type: module, jotta voimme käyttää modernia ES-moduulisyntaksia (import) vanhan require-tyylin sijaan.
mkdir jwt-auth-api && cd jwt-auth-api
npm init -y
npm pkg set type=module
npm install [email protected] [email protected] [email protected] [email protected] [email protected]
# Tarkista asennetut versiot
npm ls --depth=0
Komento npm ls --depth=0 tulostaa riippuvuuspuun. Varmista, että numerot vastaavat taulukkoa. Naulaamalla tarkat versiot (esimerkiksi [email protected]) varmistat, että projekti käyttäytyy samalla tavalla jokaisessa ympäristössä etkä saa yllätyksiä automaattisista päivityksistä.
# Odotettu tuloste
[email protected] /home/kayttaja/jwt-auth-api
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]
Vaihe 2: Ympäristömuuttujat ja vahva allekirjoitusavain
Allekirjoitusavain on koko järjestelmän tärkein salaisuus. Jos hyökkääjä saa sen, hän voi väärentää minkä tahansa tokenin ja esiintyä kenenä tahansa käyttäjänä. HS256-algoritmi vaatii vähintään 256-bittisen (32 tavua) satunnaisen avaimen. Älä koskaan käytä lyhyttä tai sanakirjasta löytyvää merkkijonoa.
Luo kaksi erillistä avainta, yksi access-tokeneille ja toinen refresh-tokeneille. Generoi ne Noden omalla kryptografisesti turvallisella satunnaislukugeneraattorilla.
# Generoi kaksi 256-bittistä avainta heksamuodossa
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Luo projektin juureen tiedosto .env ja liitä avaimet siihen. Lisää .env heti .gitignore-tiedostoon, jotta salaisuudet eivät päädy versionhallintaan. Tämä on yleisin yksittäinen tapa vuotaa avaimia.
# .env
PORT=3000
JWT_ACCESS_SECRET=4f8a...korvaa_omalla_generoidulla_arvolla
JWT_REFRESH_SECRET=9c2e...korvaa_toisella_arvolla
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=7d
NODE_ENV=development
# .gitignore
node_modules
.env
Tuotannossa älä jätä avaimia pelkkään .env-tiedostoon, vaan käytä salaisuuksien hallintaa kuten AWS Secrets Manageria, HashiCorp Vaultia tai pilvialustasi vastaavaa palvelua. Ympäristömuuttujat ovat hyvä lähtökohta kehityksessä, mutta tuotannossa ne pitää suojata kunnolla.
Vaihe 3: Käyttäjävarasto ja salasanojen tiivistäminen bcryptillä
Oikeassa sovelluksessa käyttäjät tallennetaan tietokantaan. Tässä oppaassa käytämme yksinkertaista muistinvaraista taulukkoa, jotta keskitymme JWT-logiikkaan. Korvaa se omassa projektissasi PostgreSQL- tai MongoDB-yhteydellä. Tärkeintä on, että salasanaa ei koskaan tallenneta selkokielisenä.
Tiivistämme salasanat bcryptillä. Bcrypt lisää automaattisesti satunnaisen suolan (salt) ja on tarkoituksella hidas, mikä vaikeuttaa raakaa läpikäyntiä. Lue tarkemmin salasanaturvallisuudesta ja siitä, miksi tiivistäminen on välttämätöntä. Luo tiedosto store.js.
// store.js
import bcrypt from 'bcrypt';
const users = []; // korvaa tietokannalla tuotannossa
const SALT_ROUNDS = 12; // tyotekija, 12 on hyva tasapaino 2026
export async function createUser(email, password) {
if (users.find(u => u.email === email)) {
throw new Error('Sahkoposti on jo kaytossa');
}
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const user = { id: String(users.length + 1), email, passwordHash };
users.push(user);
return { id: user.id, email: user.email };
}
export async function verifyUser(email, password) {
const user = users.find(u => u.email === email);
if (!user) return null;
const ok = await bcrypt.compare(password, user.passwordHash);
return ok ? { id: user.id, email: user.email } : null;
}
Arvo SALT_ROUNDS = 12 tarkoittaa, että bcrypt tekee 2^12 eli 4096 iteraatiokierrosta. Vuonna 2026 tämä on hyvä tasapaino turvallisuuden ja vasteajan välillä. Suuremmilla arvoilla kirjautuminen hidastuu havaittavasti. Bcryptin tarkoituksellinen hitaus on ominaisuus, ei vika: se tekee varastettujen tiivisteiden murtamisesta kallista. Vaikka hyökkääjä saisi koko tietokannan, hänen pitäisi käyttää valtava määrä laskenta-aikaa jokaisen salasanan arvaamiseen.
Tärkeä yksityiskohta: palauta sama virheilmoitus riippumatta siitä, oliko sähköposti väärä vai salasana väärä. Funktio verifyUser palauttaa null molemmissa tapauksissa, ja kirjautumisreitti vastaa samalla viestillä. Jos kertoisit erikseen “tuntematon sähköposti” ja “väärä salasana”, hyökkääjä voisi selvittää, mitkä sähköpostiosoitteet ovat järjestelmässä. Tämä on hienovarainen mutta tärkeä tapa estää käyttäjien luettelointi.
Vaihe 4: Tokenien apufunktiot ja rekisteröinti-endpoint
Keskitä tokenien luonti yhteen tiedostoon, jotta logiikkaa ei toisteta. Luo tokens.js, joka allekirjoittaa ja tarkistaa tokenit ympäristömuuttujista luetuilla avaimilla.
// tokens.js
import jwt from 'jsonwebtoken';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
export function signAccessToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
ACCESS_SECRET,
{ expiresIn: process.env.ACCESS_TOKEN_TTL, algorithm: 'HS256',
issuer: 'jwt-auth-api', audience: 'jwt-auth-client' }
);
}
export function signRefreshToken(user, tokenId) {
return jwt.sign(
{ sub: user.id, jti: tokenId },
REFRESH_SECRET,
{ expiresIn: process.env.REFRESH_TOKEN_TTL, algorithm: 'HS256',
issuer: 'jwt-auth-api', audience: 'jwt-auth-client' }
);
}
export function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET,
{ algorithms: ['HS256'], issuer: 'jwt-auth-api', audience: 'jwt-auth-client' });
}
export function verifyRefreshToken(token) {
return jwt.verify(token, REFRESH_SECRET,
{ algorithms: ['HS256'], issuer: 'jwt-auth-api', audience: 'jwt-auth-client' });
}
Huomaa kaksi ratkaisevaa tietoturvayksityiskohtaa. Ensinnäkin verify-kutsuissa määritetään algorithms: ['HS256'] nimenomaisesti. Tämä estää algoritmien sekaannushyökkäyksen, jossa hyökkääjä vaihtaa headerin algoritmiksi none tai yrittää RS256-to-HS256-temppua. Toiseksi tarkistamme aina issuer– ja audience-arvot, joten toisen palvelun tokenit hylätään.
Erilliset avaimet access- ja refresh-tokeneille ovat tarkoituksellinen valinta. Jos käyttäisit samaa avainta molempiin, refresh-tokenin vuoto vaarantaisi myös access-tokenien tarkistuksen. Erottamalla avaimet rajaat vahingon: yhden avaimen paljastuminen ei automaattisesti murra koko järjestelmää. Sama periaate koskee tuotantoympäristöjä, joissa kehitys-, testi- ja tuotantoavaimet pidetään tiukasti erillään. Älä koskaan käytä samaa avainta useassa ympäristössä, sillä silloin kehitysavaimen vuoto altistaa tuotannon.
Luo nyt pääsovellus server.js ja lisää rekisteröinti-reitti.
// server.js
import 'dotenv/config';
import express from 'express';
import cookieParser from 'cookie-parser';
import { createUser, verifyUser } from './store.js';
import {
signAccessToken, signRefreshToken,
verifyAccessToken, verifyRefreshToken
} from './tokens.js';
const app = express();
app.use(express.json());
app.use(cookieParser());
app.post('/auth/register', async (req, res) => {
const { email, password } = req.body || {};
if (!email || !password || password.length < 8) {
return res.status(400).json({ error: 'Sahkoposti ja vahintaan 8 merkin salasana vaaditaan' });
}
try {
const user = await createUser(email, password);
return res.status(201).json({ user });
} catch (e) {
return res.status(409).json({ error: e.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API kuuntelee portissa ${PORT}`));
Vaihe 5: Kirjautuminen ja access-tokenin luonti
Kirjautumisreitti tarkistaa tunnukset bcryptillä ja palauttaa access-tokenin sekä asettaa refresh-tokenin httpOnly-evästeeseen. Pidä access-token lyhytikäisenä, tässä 15 minuuttia. Lyhyt elinikä rajaa vahingon, jos token vuotaa.
// Lisaa server.js-tiedostoon, ennen app.listen-rivia
import crypto from 'crypto';
const refreshStore = new Map(); // jti -> userId, korvaa tietokannalla
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body || {};
const user = await verifyUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Virheellinen sahkoposti tai salasana' });
}
const tokenId = crypto.randomUUID();
refreshStore.set(tokenId, user.id);
const accessToken = signAccessToken(user);
const refreshToken = signRefreshToken(user, tokenId);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/auth/refresh',
maxAge: 7 * 24 * 60 * 60 * 1000
});
return res.json({ accessToken, user });
});
Refresh-token tallennetaan palvelimen refreshStore-rakenteeseen avaimella jti (token-tunniste). Tämä on tärkeää: koska tilattoman JWT:n kumoaminen on mahdotonta, refresh-tokenin kumoaminen onnistuu poistamalla sen tunniste varastosta. Access-token pysyy tilattomana ja nopeana, mutta refresh-tokeneja seurataan.
Huomaa evästeen asetukset tarkasti. httpOnly: true estää JavaScriptiä lukemasta evästettä, mikä suojaa XSS-varkaudelta. secure on päällä vain tuotannossa, koska kehityksessä käytät usein HTTP:tä localhostissa. sameSite: 'strict' estää evästeen lähtemisen muiden sivustojen pyynnöissä, mikä torjuu CSRF-hyökkäykset. path: '/auth/refresh' rajaa evästeen lähtemään vain refresh-reitille, joten se ei kulje turhaan jokaisessa pyynnössä. Nämä neljä asetusta yhdessä muodostavat refresh-tokenin suojan perustan.
Vaihe 6: Todennus-middleware suojattuja reittejä varten
Middleware lukee access-tokenin Authorization-otsakkeesta muodossa Bearer <token>, tarkistaa allekirjoituksen ja liittää käyttäjätiedot pyyntöön. Jos token puuttuu tai on virheellinen, pyyntö hylätään ennen kuin se pääsee suojattuun logiikkaan.
// Lisaa server.js-tiedostoon
function requireAuth(req, res, next) {
const header = req.headers.authorization || '';
const [scheme, token] = header.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ error: 'Bearer-token puuttuu' });
}
try {
const payload = verifyAccessToken(token);
req.user = { id: payload.sub, email: payload.email };
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token on vanhentunut', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Virheellinen token' });
}
}
Erottelemme vanhentuneen tokenin (TokenExpiredError) muista virheistä omalla virhekoodilla. Selainpuoli voi käyttää koodia TOKEN_EXPIRED tunnistaakseen, milloin kannattaa pyytää uutta access-tokenia refresh-tokenilla sen sijaan, että käyttäjä kirjataan ulos. Allekirjoituksen tarkistus nojaa samaan periaatteeseen kuin HMAC-allekirjoitukset Node.js:ssä, sillä HS256 on käytännössä HMAC-SHA256.
Vaihe 7: Suojattu reitti ja käyttäjäprofiili
Nyt voit suojata minkä tahansa reitin liittämällä requireAuth-middlewaren. Lisää profiilireitti, joka palauttaa kirjautuneen käyttäjän tiedot tokenista.
// Lisaa server.js-tiedostoon
app.get('/me', requireAuth, (req, res) => {
return res.json({
message: `Tervetuloa, ${req.user.email}`,
user: req.user
});
});
// Esimerkki rooliin perustuvasta suojauksesta
app.get('/admin', requireAuth, (req, res) => {
// Oikeudet tarkistetaan tietokannasta, ei tokenista, jotta muutokset
// astuvat voimaan heti eika vasta tokenin vanhennuttua
return res.json({ message: 'Vain todennetuille kayttajille' });
});
Huomaa kommentti admin-reitissä. Älä luota pelkästään tokeniin upotettuun rooliin arkaluontoisissa oikeustarkistuksissa. Jos käyttäjän oikeudet poistetaan, vanha token kantaa silti vanhaa roolia vanhentumiseensa asti. Kriittiset oikeudet kannattaa tarkistaa tietokannasta jokaisella pyynnöllä.
Tämä erottelu authentikaation ja autorisaation välillä on yksi yleisimmistä väärinkäsityksistä. JWT todistaa, että token on aito ja kenelle se kuuluu (autentikaatio). Se ei automaattisesti kerro, mitä käyttäjä saa tehdä (autorisaatio). Pääsynhallinta on aina sovelluksen vastuulla, ja se kannattaa toteuttaa erillisenä kerroksena todennuksen päälle. Pieni rooli kuten “lukija” voi olla tokenissa nopeuden vuoksi, mutta esimerkiksi maksun hyväksyntä tai käyttäjien poisto pitää aina varmentaa tuoreesta tietolähteestä.
Vaihe 8: Refresh-tokenit ja kierto (rotation)
Refresh-token antaa uuden access-tokenin ilman uudelleenkirjautumista. Toteutamme kierron (rotation): jokainen refresh-pyyntö mitätöi vanhan refresh-tokenin ja luo uuden. Jos vanhaa, jo käytettyä tokenia yritetään käyttää uudelleen, se on merkki varkaudesta ja koko token-perhe mitätöidään.
// Lisaa server.js-tiedostoon
app.post('/auth/refresh', (req, res) => {
const token = req.cookies?.refreshToken;
if (!token) {
return res.status(401).json({ error: 'Refresh-token puuttuu' });
}
let payload;
try {
payload = verifyRefreshToken(token);
} catch {
return res.status(401).json({ error: 'Virheellinen refresh-token' });
}
// Kierron tarkistus: onko tama jti yha voimassa?
if (!refreshStore.has(payload.jti)) {
// Uudelleenkaytto havaittu, mitatoi kaikki kayttajan tokenit
for (const [jti, uid] of refreshStore) {
if (uid === payload.sub) refreshStore.delete(jti);
}
return res.status(401).json({ error: 'Token mitatoity, kirjaudu uudelleen' });
}
// Mitatoi vanha, luo uusi
refreshStore.delete(payload.jti);
const newId = crypto.randomUUID();
refreshStore.set(newId, payload.sub);
const user = { id: payload.sub };
const accessToken = signAccessToken(user);
const refreshToken = signRefreshToken(user, newId);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/auth/refresh',
maxAge: 7 * 24 * 60 * 60 * 1000
});
return res.json({ accessToken });
});
Kierto on OWASP:n suosittelema kontrolli. Se muuttaa varastetun refresh-tokenin lähes hyödyttömäksi: heti kun joko oikea käyttäjä tai hyökkääjä käyttää tokenin, toinen osapuoli huomaa uudelleenkäytön ja koko ketju katkeaa. Tuotannossa tallenna jti-tunnisteet tietokantaan, ei muistiin, jotta ne säilyvät palvelimen uudelleenkäynnistyksen yli.
Vaihe 9: httpOnly-evästeet vai localStorage
Yksi yleisimmistä kysymyksistä on, mihin token tallennetaan selaimessa. Vaihtoehtoja on kaksi, ja niillä on erilaiset uhkamallit. Tämä taulukko vertaa niitä.
| Ominaisuus | httpOnly-eväste | localStorage |
|---|---|---|
| JavaScript pääsee tokeniin | Ei (suojaa XSS:ltä) | Kyllä (altis XSS-varkaudelle) |
| Lähtee automaattisesti pyynnöissä | Kyllä | Ei, liitettävä käsin |
| CSRF-riski | On, vaatii SameSite-suojan | Ei |
| Toimii mobiilisovelluksissa | Hankalasti | Helposti |
| Suositus 2026 | Refresh-tokenille | Vältä arkaluontoisille tokeneille |
Tässä oppaassa käytetään hybridimallia, jota pidetään parhaana käytäntönä 2026: pitkäikäinen refresh-token on httpOnly-evästeessä (JavaScript ei pääse siihen käsiksi, joten XSS ei voi varastaa sitä), ja lyhytikäinen access-token pidetään selaimen muistissa. Koska access-token vanhenee 15 minuutissa, sen vuotamisen vahinko on rajattu. Yhdistä tämä aina HTTPS-yhteyteen; lue lisää HTTPS:stä ja TLS:stä.
Koska käytämme evästettä, sameSite: 'strict' suojaa CSRF-hyökkäyksiltä rajaamalla evästeen lähtemään vain saman sivuston pyynnöissä. Tämä on tärkeä yksityiskohta, jonka moni ohittaa.
Vaihe 10: Uloskirjautuminen ja tokenin mitätöinti
Uloskirjautuminen poistaa refresh-tokenin sekä varastosta että selaimen evästeestä. Access-token ei katoa heti, mutta se vanhenee itsestään 15 minuutissa, ja koska refresh-token on mitätöity, uutta access-tokenia ei voi enää luoda.
// Lisaa server.js-tiedostoon
app.post('/auth/logout', (req, res) => {
const token = req.cookies?.refreshToken;
if (token) {
try {
const payload = verifyRefreshToken(token);
refreshStore.delete(payload.jti);
} catch {
// virheellinen token, ei tarvitse tehda mitaan
}
}
res.clearCookie('refreshToken', { path: '/auth/refresh' });
return res.json({ message: 'Uloskirjautuminen onnistui' });
});
Jos tarvitset välitöntä access-tokenien mitätöintiä (esimerkiksi pankkisovelluksessa), pidä palvelimella mustaa listaa mitätöidyistä token-tunnisteista ja tarkista se requireAuth-middlewaressa. Tämä tuo takaisin osan tilallisuudesta, mutta on joskus välttämätöntä.
Käytännössä musta lista kannattaa toteuttaa nopealla muistivarastolla kuten Redisillä, jossa jokainen mitätöity token-tunniste säilyy vain tokenin jäljellä olevan eliniän ajan. Koska access-token vanhenee 15 minuutissa, mustalla listalla on enimmillään 15 minuutin verran tunnisteita kerrallaan, joten se pysyy pienenä. Tämä on hyvä kompromissi: säilytät JWT:n nopeuden valtaosassa pyyntöjä, mutta saat välittömän mitätöinnin silloin kun sitä todella tarvitset.
Vaihe 11: Allekirjoitusalgoritmit, HS256 vs RS256 vs EdDSA
Tähän asti olemme käyttäneet HS256:ta, joka käyttää yhtä jaettua salaisuutta sekä allekirjoitukseen että tarkistukseen. Se on yksinkertainen ja nopea, kun sama palvelin tekee molemmat. Jos taas eri palvelu allekirjoittaa ja toinen tarkistaa, tarvitset epäsymmetristä algoritmia, jossa yksityinen avain allekirjoittaa ja julkinen avain tarkistaa.
| Algoritmi | Tyyppi | Avain | Paras käyttötapaus |
|---|---|---|---|
| HS256 | Symmetrinen (HMAC-SHA256) | Yksi jaettu salaisuus | Yksi backend allekirjoittaa ja tarkistaa |
| RS256 | Epäsymmetrinen (RSA) | Yksityinen + julkinen | Issuer ja verifier eri palveluissa |
| ES256 | Epäsymmetrinen (ECDSA P-256) | Yksityinen + julkinen | Pienemmät avaimet, mobiili |
| EdDSA | Epäsymmetrinen (Ed25519) | Yksityinen + julkinen | Moderni, nopea, suositeltu uusiin |
| none | Ei allekirjoitusta | Ei mitään | Älä koskaan käytä, vakava aukko |
Näin vaihdat RS256:een. Generoi avainpari OpenSSL:llä, allekirjoita yksityisellä avaimella ja tarkista julkisella. Julkisen avaimen voi jakaa vapaasti palveluille, jotka tarkistavat tokeneita.
# Generoi RSA-avainpari
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
// RS256-allekirjoitus ja tarkistus
import fs from 'fs';
import jwt from 'jsonwebtoken';
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
const token = jwt.sign({ sub: '1' }, privateKey, {
algorithm: 'RS256', expiresIn: '15m'
});
// Tarkistuksessa salli VAIN RS256, esta algoritmien sekaannus
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Ratkaiseva sääntö: tarkistuksessa määritä aina sallitut algoritmit nimenomaisesti (algorithms: ['RS256']). Jos jätät tämän pois, hyökkääjä voi yrittää syöttää julkisen avaimen HMAC-salaisuutena ja väärentää tokeneita. Tämä RS256-to-HS256-sekaannus on yksi tunnetuimmista JWT-aukoista. JWT:n epäsymmetrinen allekirjoitus toimii samalla periaatteella kuin digitaaliset allekirjoitukset yleisesti.
Vaihe 12: Testaus curlilla ja vienti tuotantoon
Käynnistä palvelin komennolla node server.js ja testaa koko kierto curlilla. Ensin rekisteröinti, sitten kirjautuminen, sitten suojattu reitti.
# 1. Rekisteroi kayttaja
curl -s -X POST http://localhost:3000/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"vahvaSalasana123"}'
# 2. Kirjaudu sisaan, talleta evaste ja access-token
curl -s -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-c cookies.txt \
-d '{"email":"[email protected]","password":"vahvaSalasana123"}'
# 3. Kayta access-tokenia suojattuun reittiin
TOKEN="liita_saamasi_accessToken_tahan"
curl -s http://localhost:3000/me \
-H "Authorization: Bearer $TOKEN"
# Odotettu tuloste kohdasta 3
{"message":"Tervetuloa, [email protected]","user":{"id":"1","email":"[email protected]"}}
# Refresh-pyynto evasteella
curl -s -X POST http://localhost:3000/auth/refresh -b cookies.txt
{"accessToken":"eyJhbGciOiJIUzI1NiI..."}
Kun kaikki vaiheet toimivat, sinulla on täysi JWT-todennusjärjestelmä. Ennen tuotantoon vientiä varmista, että NODE_ENV=production on asetettu (jolloin evästeet käyttävät secure-lippua), palvelin on HTTPS:n takana, refresh-tokenit tallennetaan tietokantaan ja avaimet tulevat salaisuuksien hallinnasta.
JWT-tietoturva: 5 yleistä virhettä, jotka pitää välttää
Suurin osa JWT-haavoittuvuuksista syntyy samoista perusvirheistä. OWASP:n JWT-ohjeistus nostaa esiin nämä toistuvasti.
- Algoritmin jättäminen tarkistamatta. Jos et anna
verify-kutsullealgorithms-listaa, kirjasto saattaa hyväksyä headerin ilmoittaman algoritmin. Hyökkääjä voi asettaaalg: nonetai vaihtaa RS256:n HS256:een. Salli aina vain yksi tunnettu algoritmi. - Arkaluontoinen tieto payloadissa. Payload on koodattu, ei salattu. Salasanat, henkilötunnukset tai luottokorttitiedot tokenissa vuotavat kaikille, jotka näkevät tokenin.
- Liian pitkä access-tokenin elinikä. Tunteja tai päiviä kestävä access-token tarkoittaa, että varastetulla tokenilla voi tehdä vahinkoa pitkään. Pidä se 15 minuutissa ja käytä refresh-tokenia.
- Heikko tai kovakoodattu allekirjoitusavain. Lyhyt avain on murrettavissa raa’alla voimalla. Lähdekoodiin kovakoodattu avain vuotaa heti, kun repositorio jaetaan. Käytä 256-bittistä satunnaisavainta ympäristömuuttujassa.
- Tokenin tallennus localStorageen ilman XSS-suojaa. Jos sivustolla on XSS-aukko, hyökkääjän skripti lukee localStoragessa olevan tokenin. Käytä httpOnly-evästettä refresh-tokenille.
Vianetsintä: 8 yleistä ongelmaa ja ratkaisut
Alla tavallisimmat virheilmoitukset, joihin törmäät, ja niiden syyt.
| Virhe / oire | Syy | Ratkaisu |
|---|---|---|
invalid signature | Allekirjoitukseen ja tarkistukseen eri avain | Varmista, että sama JWT_ACCESS_SECRET on käytössä |
jwt expired | Access-token vanheni | Pyydä uusi token /auth/refresh-reitiltä |
jwt malformed | Token ei ole kolmiosainen tai on katkennut | Tarkista, että Bearer-etuliite ja token erotellaan oikein |
secretOrPrivateKey must have a value | .env ei latautunut | Varmista import 'dotenv/config' ennen muuta koodia |
jwt audience invalid | audience ei täsmää | Käytä samaa arvoa allekirjoituksessa ja tarkistuksessa |
| Eväste ei tallennu selaimeen | secure: true ilman HTTPS:ää | Kehityksessä pidä NODE_ENV=development |
req.body on undefined | express.json() puuttuu | Lisää app.use(express.json()) |
| 401 heti kirjautumisen jälkeen | Kellojen ero palvelimien välillä | Synkronoi NTP:llä tai salli pieni clockTolerance |
Jos näet invalid signature -virheen vaikka avaimet näyttävät oikeilta, tarkista ettei .env-tiedostoon ole jäänyt rivinvaihtoa tai lainausmerkkejä avaimen ympärille. dotenv lukee arvon kirjaimellisesti. Yleinen ansa on myös, että kehitysympäristössä ajetaan vahingossa kahta palvelinta eri avaimilla, jolloin yhdessä luotu token ei kelpaa toisessa. Pysäytä kaikki node-prosessit ja käynnistä palvelin uudelleen, jos epäilet tätä.
Virheiden lokitus turvallisesti
Kun tutkit todennusvirheitä, älä koskaan tulosta koko tokenia lokiin. Token on käytännössä salasana, ja lokit päätyvät usein paikkoihin, joissa niitä lukee moni. Lokita sen sijaan vain virhetyyppi ja tarvittaessa tokenin jti-tunniste, jonka avulla voit jäljittää tapahtuman ilman, että itse token vuotaa. Tämä on pieni mutta tärkeä tapa, joka erottaa kokeneen kehittäjän aloittelijasta.
// Turvallinen lokitus todennusvirheessa
catch (err) {
console.warn('Todennus epaonnistui:', err.name); // EI koko tokenia
return res.status(401).json({ error: 'Virheellinen token' });
}
jsonwebtoken-kirjaston haavoittuvuushistoria
Kirjaston historia osoittaa, miksi versioiden ajan tasalla pitäminen on tärkeää. Joulukuussa 2022 julkaistiin useita haavoittuvuuksia, jotka korjattiin versiossa 9.0.0. Niihin kuuluivat muun muassa CVE-2022-23529 (avaimen tyypin tarkistuksen ohitus) ja siihen liittyvät neuvonnot CVE-2022-23539 ja CVE-2022-23540. Vanhoissa 8.x-versioissa nämä jäivät korjaamatta.
Käytä siksi aina vähintään versiota 9.0.0, mieluiten uusinta korjattua julkaisua (tätä kirjoitettaessa 9.0.3). Älä koskaan kirjoita omaa JWT-purkulogiikkaa: virheet allekirjoituksen tarkistuksessa ovat helppoja tehdä ja vaikeita huomata. Voit seurata kirjaston julkaisuja sen GitHub-sivulla ja lukea OWASP:n suositukset JWT-tietoturvasta.
Suorituskyky ja skaalautuvuus tuotannossa
JWT:n suurin suorituskykyetu on, että access-tokenin tarkistus on pelkkä HMAC-laskenta ilman tietokantahakua. Yksi palvelinydin tarkistaa tuhansia HS256-tokeneita sekunnissa. Tämä tekee JWT:stä erinomaisen valinnan rajapinnoille, joissa pyyntöjä tulee paljon ja jokainen niistä pitää todentaa. Vertailun vuoksi: palvelinistunto vaatii usein hakuun Redis- tai tietokantakutsun, joka lisää muutaman millisekunnin jokaiseen pyyntöön.
Algoritmin valinta vaikuttaa nopeuteen. HS256 (symmetrinen) on selvästi nopein sekä allekirjoituksessa että tarkistuksessa. RS256 (RSA) on raskaampi, erityisesti allekirjoituksessa, koska RSA-laskenta on hidasta. EdDSA (Ed25519) tarjoaa epäsymmetrisen allekirjoituksen lähes symmetrisen nopeudella ja pienemmillä avaimilla, minkä vuoksi se on suositeltava valinta uusiin epäsymmetrisiin järjestelmiin. Jos rajapintaasi tulee kymmeniä tuhansia pyyntöjä sekunnissa, algoritmin valinta alkaa näkyä prosessorikuormassa.
Pidä tokenit pieninä. Jokainen claim kasvattaa tokenia, ja token kulkee jokaisen pyynnön mukana otsakkeessa. Suuri token syö kaistaa ja hidastaa pyyntöjä. Laita tokeniin vain välttämätön: käyttäjän tunniste, vanhentumisaika ja ehkä rooli. Hae kaikki muu tarvittava tieto tietokannasta tunnisteen perusteella. Tämä myös pienentää riskiä, että arkaluontoista tietoa vuotaa koodatun payloadin kautta.
Edistyneet vinkit tuotantokäyttöön
Avainten kierto ja kid-otsake
Tuotannossa allekirjoitusavain kannattaa vaihtaa säännöllisesti. Lisää tokenin headeriin kid-tunniste (key ID), joka kertoo, millä avaimella token allekirjoitettiin. Näin voit pitää useaa avainta voimassa siirtymäkauden ajan ja tarkistaa kunkin tokenin oikealla avaimella. Tämä mahdollistaa avainten vaihdon ilman, että kaikki käyttäjät kirjautuvat ulos kerralla.
Pyyntörajoitus kirjautumiseen
Suojaa kirjautumisreitti raa’alta läpikäynniltä pyyntörajoituksella. Esimerkiksi express-rate-limit rajaa yhden IP:n yritykset esimerkiksi viiteen viiden minuutin aikana. Yhdistä tämä bcryptin hitauteen, niin salasanojen arvaaminen muuttuu epäkäytännölliseksi. Tämä on tärkeä lisä, sillä JWT ei itsessään suojaa heikoilta salasanoilta.
Kellotoleranssi hajautetuissa järjestelmissä
Jos palvelimien kellot eroavat hieman, juuri luotu token voi näyttää vanhentuneelta tai vielä voimaan tulemattomalta. Anna verify-kutsulle clockTolerance: 5 (sekuntia), niin pieni ero ei kaada todennusta. Pidä toleranssi kuitenkin pienenä, sillä liian suuri arvo heikentää exp-tarkistuksen tehoa. Paras ratkaisu on synkronoida kaikkien palvelimien kellot NTP:llä, jolloin toleranssia ei juuri tarvita.
Tokenin tallennus mobiilisovelluksessa
Mobiilisovelluksissa httpOnly-evästeet eivät toimi yhtä luontevasti kuin selaimessa, joten token tallennetaan yleensä laitteen suojattuun avainvarastoon. iOS:ssä tämä on Keychain ja Androidissa EncryptedSharedPreferences tai Keystore. Älä koskaan tallenna tokenia tavalliseen tekstitiedostoon tai suojaamattomaan asetusvarastoon. Suojattu avainvarasto salaa tokenin laitteen laitteistopohjaisella avaimella, joten muut sovellukset eivät pääse siihen käsiksi edes silloin, kun laite on juurrutettu tai jailbreikattu.
Claimien suunnittelu
Hyvä token sisältää standardiclaimit, joita kirjastot ymmärtävät: sub (subjekti eli käyttäjä), iat (luontiaika), exp (vanhentuminen), iss (myöntäjä), aud (vastaanottaja) ja jti (yksilöllinen tunniste). Omat claimit kannattaa nimetä kuvaavasti ja pitää lyhyinä. Vältä koko käyttäjäobjektin ahtamista tokeniin. Mitä vähemmän token sisältää, sitä pienempi se on ja sitä vähemmän tietoa vuotaa, jos token päätyy vääriin käsiin. Kun lisäät rooleja tai oikeuksia tokeniin, muista että ne jäätyvät tokenin luontihetkeen, joten arkaluontoiset oikeudet kannattaa silti tarkistaa tietokannasta.
Usein kysytyt kysymykset
Onko JWT turvallinen istuntojen korvaaja?
JWT on turvallinen, kun se toteutetaan oikein: lyhyt access-tokenin elinikä, refresh-tokenien kierto, allekirjoitusalgoritmin nimenomainen tarkistus ja HTTPS. JWT ei kuitenkaan ole automaattisesti parempi kuin palvelinpuolen istunto. Jos tarvitset välitöntä uloskirjautumista kaikilta laitteilta, tilalliset istunnot voivat olla yksinkertaisempia.
Mihin token kannattaa tallentaa selaimessa?
Refresh-token kannattaa pitää httpOnly-evästeessä, johon JavaScript ei pääse käsiksi, joten XSS ei voi varastaa sitä. Lyhytikäinen access-token voidaan pitää selaimen muistissa. Vältä pitkäikäisten tokenien tallentamista localStorageen.
Kuinka pitkä access-tokenin eliniän pitäisi olla?
Yleinen suositus 2026 on 15 minuuttia. Lyhyt elinikä rajaa varastetun tokenin vahingon, ja refresh-token huolehtii siitä, ettei käyttäjän tarvitse kirjautua uudelleen kovin usein. Refresh-token voi kestää esimerkiksi 7 päivää.
Voiko JWT:n kumota ennen vanhentumista?
Pelkkää tilatonta access-tokenia ei voi suoraan kumota, mutta refresh-tokenit voi mitätöidä poistamalla niiden tunnisteet palvelimelta. Jos tarvitset välitöntä access-tokenin mitätöintiä, ylläpidä palvelimella mustaa listaa mitätöidyistä token-tunnisteista ja tarkista se jokaisella pyynnöllä.
HS256 vai RS256?
Käytä HS256:ta, kun sama backend sekä allekirjoittaa että tarkistaa tokenit. Valitse RS256 tai EdDSA, kun eri palvelut hoitavat allekirjoituksen ja tarkistuksen, koska epäsymmetrisessä mallissa julkisen avaimen voi jakaa turvallisesti ilman, että se vaarantaa allekirjoituskykyä.
Tarvitseeko JWT tietokantaa?
Access-tokenien tarkistus ei vaadi tietokantaa, mikä on JWT:n pääetu. Refresh-tokenien kierto ja mitätöinti sen sijaan kannattaa tukea tietokannalla tuotannossa, jotta tilatieto säilyy palvelimen uudelleenkäynnistyksen yli ja skaalautuu usealle palvelimelle.
Mitä alg:none tarkoittaa ja miksi se on vaarallinen?
Arvo alg: none kertoo, että token ei ole allekirjoitettu. Jos palvelin hyväksyy tämän, kuka tahansa voi luoda väärennetyn tokenin millä tahansa sisällöllä. Estä se aina määrittämällä sallitut algoritmit verify-kutsussa, jolloin allekirjoittamattomat tokenit hylätään.
Toimiiko tämä Express 4:llä?
Periaate on sama, mutta Express 5 muutti muutamia oletuksia, kuten asynkronisten virheiden käsittelyä. Tämän oppaan koodi on testattu Express 5.2.1:llä. Express 4:llä koodi toimii pienin muutoksin, mutta uusiin projekteihin kannattaa valita Express 5.
Yhteenveto
Rakensit täyden JWT-todennusjärjestelmän Node.js:llä ja Express 5:llä: rekisteröinti bcrypt-tiivisteillä, kirjautuminen, lyhytikäiset access-tokenit, httpOnly-evästeessä kulkevat refresh-tokenit ja niiden kierto. Tärkeimmät tietoturvaperiaatteet olivat allekirjoitusalgoritmin nimenomainen tarkistus, vahva satunnaisavain, lyhyt tokenin elinikä ja salaisuuksien pitäminen poissa lähdekoodista. Nämä neljä asiaa erottavat tuotantokelpoisen toteutuksen aukollisesta.
Voit laajentaa projektia korvaamalla muistinvaraiset varastot tietokannalla, lisäämällä pyyntörajoituksen ja siirtymällä epäsymmetriseen allekirjoitukseen mikropalveluissa. JWT:n perusta pysyy samana riippumatta siitä, kuinka suureksi järjestelmäsi kasvaa.
Muista lopuksi, että tokeniin liittyvä tietoturva on ketju, jonka heikoin lenkki ratkaisee. Vahva avain ei auta, jos token kulkee salaamattomana HTTP:n yli. Lyhyt elinikä ei auta, jos refresh-token vuotaa localStoragesta XSS-aukon kautta. Algoritmin tarkistus ei auta, jos avain on kovakoodattu repositorioon. Käy tämän oppaan tarkistuslista läpi ennen jokaista tuotantojulkaisua: vahva satunnaisavain ympäristömuuttujassa, nimenomainen algoritmin tarkistus, lyhyt access-tokenin elinikä, refresh-tokenin kierto, httpOnly-eväste ja HTTPS kaiken päällä. Kun nämä kuusi asiaa ovat kunnossa, JWT-toteutuksesi kestää valtaosan todellisista hyökkäyksistä.
Aiheeseen liittyvää
- Turvalliset sessiot Node.js:ssä: 10 vaihetta
- HMAC Node.js:ssä: 10 vaihetta, 30 min
- Salasanaturvallisuus: vahvat salasanat ja niiden suojaus
- Digitaaliset allekirjoitukset: tiivisteet ja epäsymmetriset avaimet
- HTTPS ja TLS: miten salattu yhteys suojaa sinua
- Verkkoturvallisuus: opas digitaalisen elämän suojaamiseen
Lähteet ja lisälukemista: RFC 7519 (JWT-standardi), jwt.io, OWASP Top Ten.




