{"id":89,"date":"2026-06-15T16:56:28","date_gmt":"2026-06-15T16:56:28","guid":{"rendered":"https:\/\/shattered.io\/fi\/2026\/06\/15\/jwt-todennus-nodejs\/"},"modified":"2026-06-16T08:40:08","modified_gmt":"2026-06-16T08:40:08","slug":"jwt-todennus-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fi\/jwt-todennus-nodejs\/","title":{"rendered":"JWT-todennus Node.js:ss\u00e4: 12 vaihetta, 40 min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">JWT eli JSON Web Token on yleisin tapa todentaa k\u00e4ytt\u00e4ji\u00e4 moderneissa Node.js-rajapinnoissa. Token kulkee jokaisen pyynn\u00f6n mukana, palvelin tarkistaa allekirjoituksen eik\u00e4 istuntoja tarvitse tallentaa tietokantaan. T\u00e4m\u00e4 opas rakentaa t\u00e4yden kirjautumisj\u00e4rjestelm\u00e4n Express 5:ll\u00e4 ja <code>jsonwebtoken<\/code>-kirjastolla: rekister\u00f6inti, kirjautuminen, suojatut reitit, refresh-tokenit ja niiden kierto. K\u00e4yt\u00e4t versioita, jotka ovat tuotantokelpoisia kes\u00e4kuussa 2026, ja v\u00e4lt\u00e4t ne tietoturva-aukot, jotka kaatavat suurimman osan aloittelijoiden JWT-toteutuksista.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ty\u00f6 vie noin 40 minuuttia. Lopputuloksena on toimiva projekti, jonka voit testata <code>curl<\/code>-komennoilla ja vied\u00e4 sellaisenaan palvelimelle. P\u00e4ivitetty 15. kes\u00e4kuuta 2026.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mita-jwt-on\">Mit\u00e4 JWT on ja miksi sit\u00e4 kannattaa k\u00e4ytt\u00e4\u00e4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JWT on standardoitu (RFC 7519) tapa siirt\u00e4\u00e4 v\u00e4itteit\u00e4 eli claimeja kahden osapuolen v\u00e4lill\u00e4 allekirjoitetussa muodossa. Kun k\u00e4ytt\u00e4j\u00e4 kirjautuu sis\u00e4\u00e4n, palvelin luo tokenin, allekirjoittaa sen salaisella avaimella ja l\u00e4hett\u00e4\u00e4 sen selaimelle. Selain liitt\u00e4\u00e4 tokenin jokaiseen seuraavaan pyynt\u00f6\u00f6n. Palvelin tarkistaa allekirjoituksen ja luottaa tokenin sis\u00e4lt\u00f6\u00f6n ilman tietokantahakua.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4m\u00e4 tilattomuus on JWT:n suurin etu perinteisiin istuntoihin verrattuna. Palvelin ei pid\u00e4 yll\u00e4 istuntotaulua, joten sovellus skaalautuu vaakasuunnassa ilman jaettua istuntovarastoa kuten Redisi\u00e4. Sama token toimii usealla palvelimella, kunhan ne jakavat saman allekirjoitusavaimen. Mikropalveluissa ja mobiilisovelluksissa t\u00e4m\u00e4 on ratkaiseva ero. Jos haluat verrata l\u00e4hestymistapaa palvelinpuolen istuntoihin, lue erillinen oppaamme <a href=\"\/turvalliset-sessiot-nodejs\/\">turvallisista sessioista Node.js:ss\u00e4<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tilattomuudella on hintansa. Koska palvelin ei seuraa tokeneita, vanhentunutta tai varastettua tokenia ei voi suoraan kumota ennen sen vanhentumisaikaa. T\u00e4h\u00e4n ongelmaan vastaa kahden tokenin malli (lyhytik\u00e4inen access-token ja pidempi refresh-token), jonka rakennat vaiheessa 8.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"jwt-vai-istunto\">JWT vai palvelinistunto: milloin kumpaakin<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">JWT ei ole automaattisesti parempi kuin perinteinen istunto, vaikka markkinointi joskus n\u00e4in antaa ymm\u00e4rt\u00e4\u00e4. Valinta riippuu arkkitehtuurista. T\u00e4m\u00e4 taulukko auttaa p\u00e4\u00e4tt\u00e4m\u00e4\u00e4n.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Tilanne<\/th><th>JWT<\/th><th>Palvelinistunto<\/th><\/tr><\/thead><tbody>\n<tr><td>Useita backend-palvelimia<\/td><td>Skaalautuu helposti, ei jaettua varastoa<\/td><td>Vaatii jaetun istuntovaraston (Redis)<\/td><\/tr>\n<tr><td>Mobiilisovellus tai SPA<\/td><td>Luonteva valinta, token kulkee otsakkeessa<\/td><td>Ev\u00e4ste-pohjainen, kankeampi<\/td><\/tr>\n<tr><td>V\u00e4lit\u00f6n uloskirjautuminen kaikilta laitteilta<\/td><td>Vaatii lis\u00e4ty\u00f6t\u00e4 (musta lista)<\/td><td>Sis\u00e4\u00e4nrakennettu, poista istunto<\/td><\/tr>\n<tr><td>Mikropalvelut, eri tiimit<\/td><td>Julkinen avain tarkistaa, ei jaettua tilaa<\/td><td>Hankala, vaatii keskitetyn istuntopalvelun<\/td><\/tr>\n<tr><td>Yksi monoliittinen sovellus<\/td><td>Toimii, mutta ei pakollinen<\/td><td>Yksinkertainen ja riitt\u00e4v\u00e4<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Nyrkkis\u00e4\u00e4nt\u00f6: valitse JWT, kun sinulla on useita palveluita tai mobiiliasiakkaita, jotka kuluttavat samaa rajapintaa. Valitse palvelinistunto, kun rakennat yht\u00e4 perinteist\u00e4 verkkosovellusta ja haluat yksinkertaisen uloskirjautumisen. T\u00e4m\u00e4 opas keskittyy JWT:hen, koska se on yleisin valinta moderneissa API-pohjaisissa sovelluksissa, mutta on hyv\u00e4 tuntea molemmat. K\u00e4yt\u00e4nn\u00f6ss\u00e4 monet tuotantoj\u00e4rjestelm\u00e4t yhdist\u00e4v\u00e4t molemmat: JWT rajapintakutsuihin ja palvelinistunto hallintapaneeliin.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"jwt-rakenne\">JWT:n rakenne: header, payload ja allekirjoitus<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">JWT koostuu kolmesta osasta, jotka erotetaan pisteell\u00e4: <code>header.payload.signature<\/code>. Jokainen osa on Base64URL-koodattu, mik\u00e4 eroaa tavallisesta Base64:st\u00e4 siten, ett\u00e4 se on turvallinen URL-osoitteissa. Header kertoo tokenin tyypin ja allekirjoitusalgoritmin, esimerkiksi <code>{\"alg\":\"HS256\",\"typ\":\"JWT\"}<\/code>. Payload sis\u00e4lt\u00e4\u00e4 claimit kuten <code>sub<\/code> (k\u00e4ytt\u00e4j\u00e4n tunniste), <code>iat<\/code> (luontiaika) ja <code>exp<\/code> (vanhentumisaika). Allekirjoitus lasketaan headerista ja payloadista salaisella avaimella.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4rke\u00e4 ja usein v\u00e4\u00e4rin ymm\u00e4rretty seikka: payload on vain koodattu, ei salattu. Kuka tahansa voi purkaa Base64URL-koodauksen ja lukea sis\u00e4ll\u00f6n. \u00c4l\u00e4 koskaan laita tokeniin salasanoja, henkil\u00f6tunnuksia tai muuta arkaluontoista tietoa. Allekirjoitus takaa eheyden (sis\u00e4lt\u00f6\u00e4 ei ole muutettu), ei luottamuksellisuutta. Jos tarvitset salausta, k\u00e4yt\u00e4 JWE-muotoa tavallisen JWS:n sijaan.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"esitiedot\">Esitiedot ja vaaditut versiot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tarvitset perustiedot JavaScriptist\u00e4 ja komentorivist\u00e4 sek\u00e4 asennettuna Node.js:n. Alla olevat versiot ovat t\u00e4m\u00e4n oppaan testiymp\u00e4rist\u00f6 kes\u00e4kuussa 2026. K\u00e4yt\u00e4 Active LTS -versiota, \u00e4l\u00e4 koskaan pelkk\u00e4\u00e4 Current-haaraa tuotannossa.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Komponentti<\/th><th>Versio<\/th><th>Rooli projektissa<\/th><\/tr><\/thead><tbody>\n<tr><td>Node.js<\/td><td>24.16.0 LTS (Krypton)<\/td><td>Suoritusymp\u00e4rist\u00f6, julkaistu 21.5.2026<\/td><\/tr>\n<tr><td>npm<\/td><td>11.x (tulee Noden mukana)<\/td><td>Pakettienhallinta<\/td><\/tr>\n<tr><td>express<\/td><td>5.2.1<\/td><td>HTTP-palvelin ja reititys<\/td><\/tr>\n<tr><td>jsonwebtoken<\/td><td>9.0.3<\/td><td>Tokenien luonti ja todennus<\/td><\/tr>\n<tr><td>bcrypt<\/td><td>6.0.0<\/td><td>Salasanojen tiivist\u00e4minen<\/td><\/tr>\n<tr><td>cookie-parser<\/td><td>1.4.7<\/td><td>Ev\u00e4steiden lukeminen<\/td><\/tr>\n<tr><td>dotenv<\/td><td>17.4.2<\/td><td>Ymp\u00e4rist\u00f6muuttujien lataus<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Jos k\u00e4yt\u00e4t vanhempaa Node.js 22 LTS -versiota, se saa tietoturvap\u00e4ivityksi\u00e4 huhtikuuhun 2027 asti ja kelpaa my\u00f6s. Voit tarkistaa Noden elinkaaren <a href=\"https:\/\/nodejs.org\/en\/about\/previous-releases\" target=\"_blank\" rel=\"noopener nofollow\">virallisesta julkaisutaulukosta<\/a>. Express 5 on uusin p\u00e4\u00e4versio, ja se vaatii Node.js 18:aa tai uudempaa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-1\">Vaihe 1: Projektin alustus ja riippuvuuksien asennus<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Luo projektikansio ja alusta npm-projekti. Asetamme <code>type: module<\/code>, jotta voimme k\u00e4ytt\u00e4\u00e4 modernia ES-moduulisyntaksia (<code>import<\/code>) vanhan <code>require<\/code>-tyylin sijaan.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir jwt-auth-api &amp;&amp; cd jwt-auth-api\nnpm init -y\nnpm pkg set type=module\nnpm install express@5.2.1 jsonwebtoken@9.0.3 bcrypt@6.0.0 cookie-parser@1.4.7 dotenv@17.4.2\n\n# Tarkista asennetut versiot\nnpm ls --depth=0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Komento <code>npm ls --depth=0<\/code> tulostaa riippuvuuspuun. Varmista, ett\u00e4 numerot vastaavat taulukkoa. Naulaamalla tarkat versiot (esimerkiksi <code>express@5.2.1<\/code>) varmistat, ett\u00e4 projekti k\u00e4ytt\u00e4ytyy samalla tavalla jokaisessa ymp\u00e4rist\u00f6ss\u00e4 etk\u00e4 saa yll\u00e4tyksi\u00e4 automaattisista p\u00e4ivityksist\u00e4.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Odotettu tuloste\njwt-auth-api@1.0.0 \/home\/kayttaja\/jwt-auth-api\n\u251c\u2500\u2500 bcrypt@6.0.0\n\u251c\u2500\u2500 cookie-parser@1.4.7\n\u251c\u2500\u2500 dotenv@17.4.2\n\u251c\u2500\u2500 express@5.2.1\n\u2514\u2500\u2500 jsonwebtoken@9.0.3<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-2\">Vaihe 2: Ymp\u00e4rist\u00f6muuttujat ja vahva allekirjoitusavain<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Allekirjoitusavain on koko j\u00e4rjestelm\u00e4n t\u00e4rkein salaisuus. Jos hy\u00f6kk\u00e4\u00e4j\u00e4 saa sen, h\u00e4n voi v\u00e4\u00e4rent\u00e4\u00e4 mink\u00e4 tahansa tokenin ja esiinty\u00e4 kenen\u00e4 tahansa k\u00e4ytt\u00e4j\u00e4n\u00e4. HS256-algoritmi vaatii v\u00e4hint\u00e4\u00e4n 256-bittisen (32 tavua) satunnaisen avaimen. \u00c4l\u00e4 koskaan k\u00e4yt\u00e4 lyhytt\u00e4 tai sanakirjasta l\u00f6ytyv\u00e4\u00e4 merkkijonoa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Luo kaksi erillist\u00e4 avainta, yksi access-tokeneille ja toinen refresh-tokeneille. Generoi ne Noden omalla kryptografisesti turvallisella satunnaislukugeneraattorilla.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Generoi kaksi 256-bittist\u00e4 avainta heksamuodossa\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\nnode -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luo projektin juureen tiedosto <code>.env<\/code> ja liit\u00e4 avaimet siihen. Lis\u00e4\u00e4 <code>.env<\/code> heti <code>.gitignore<\/code>-tiedostoon, jotta salaisuudet eiv\u00e4t p\u00e4\u00e4dy versionhallintaan. T\u00e4m\u00e4 on yleisin yksitt\u00e4inen tapa vuotaa avaimia.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nPORT=3000\nJWT_ACCESS_SECRET=4f8a...korvaa_omalla_generoidulla_arvolla\nJWT_REFRESH_SECRET=9c2e...korvaa_toisella_arvolla\nACCESS_TOKEN_TTL=15m\nREFRESH_TOKEN_TTL=7d\nNODE_ENV=development<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\nnode_modules\n.env<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tuotannossa \u00e4l\u00e4 j\u00e4t\u00e4 avaimia pelkk\u00e4\u00e4n <code>.env<\/code>-tiedostoon, vaan k\u00e4yt\u00e4 salaisuuksien hallintaa kuten AWS Secrets Manageria, HashiCorp Vaultia tai pilvialustasi vastaavaa palvelua. Ymp\u00e4rist\u00f6muuttujat ovat hyv\u00e4 l\u00e4ht\u00f6kohta kehityksess\u00e4, mutta tuotannossa ne pit\u00e4\u00e4 suojata kunnolla.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-3\">Vaihe 3: K\u00e4ytt\u00e4j\u00e4varasto ja salasanojen tiivist\u00e4minen bcryptill\u00e4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Oikeassa sovelluksessa k\u00e4ytt\u00e4j\u00e4t tallennetaan tietokantaan. T\u00e4ss\u00e4 oppaassa k\u00e4yt\u00e4mme yksinkertaista muistinvaraista taulukkoa, jotta keskitymme JWT-logiikkaan. Korvaa se omassa projektissasi PostgreSQL- tai MongoDB-yhteydell\u00e4. T\u00e4rkeint\u00e4 on, ett\u00e4 salasanaa ei koskaan tallenneta selkokielisen\u00e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tiivist\u00e4mme salasanat bcryptill\u00e4. Bcrypt lis\u00e4\u00e4 automaattisesti satunnaisen suolan (salt) ja on tarkoituksella hidas, mik\u00e4 vaikeuttaa raakaa l\u00e4pik\u00e4ynti\u00e4. Lue tarkemmin <a href=\"\/salasanaturvallisuus\/\">salasanaturvallisuudesta<\/a> ja siit\u00e4, miksi tiivist\u00e4minen on v\u00e4ltt\u00e4m\u00e4t\u00f6nt\u00e4. Luo tiedosto <code>store.js<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ store.js\nimport bcrypt from 'bcrypt';\n\nconst users = []; \/\/ korvaa tietokannalla tuotannossa\nconst SALT_ROUNDS = 12; \/\/ tyotekija, 12 on hyva tasapaino 2026\n\nexport async function createUser(email, password) {\n  if (users.find(u =&gt; u.email === email)) {\n    throw new Error('Sahkoposti on jo kaytossa');\n  }\n  const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);\n  const user = { id: String(users.length + 1), email, passwordHash };\n  users.push(user);\n  return { id: user.id, email: user.email };\n}\n\nexport async function verifyUser(email, password) {\n  const user = users.find(u =&gt; u.email === email);\n  if (!user) return null;\n  const ok = await bcrypt.compare(password, user.passwordHash);\n  return ok ? { id: user.id, email: user.email } : null;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Arvo <code>SALT_ROUNDS = 12<\/code> tarkoittaa, ett\u00e4 bcrypt tekee 2^12 eli 4096 iteraatiokierrosta. Vuonna 2026 t\u00e4m\u00e4 on hyv\u00e4 tasapaino turvallisuuden ja vasteajan v\u00e4lill\u00e4. Suuremmilla arvoilla kirjautuminen hidastuu havaittavasti. Bcryptin tarkoituksellinen hitaus on ominaisuus, ei vika: se tekee varastettujen tiivisteiden murtamisesta kallista. Vaikka hy\u00f6kk\u00e4\u00e4j\u00e4 saisi koko tietokannan, h\u00e4nen pit\u00e4isi k\u00e4ytt\u00e4\u00e4 valtava m\u00e4\u00e4r\u00e4 laskenta-aikaa jokaisen salasanan arvaamiseen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4rke\u00e4 yksityiskohta: palauta sama virheilmoitus riippumatta siit\u00e4, oliko s\u00e4hk\u00f6posti v\u00e4\u00e4r\u00e4 vai salasana v\u00e4\u00e4r\u00e4. Funktio <code>verifyUser<\/code> palauttaa <code>null<\/code> molemmissa tapauksissa, ja kirjautumisreitti vastaa samalla viestill\u00e4. Jos kertoisit erikseen &#8220;tuntematon s\u00e4hk\u00f6posti&#8221; ja &#8220;v\u00e4\u00e4r\u00e4 salasana&#8221;, hy\u00f6kk\u00e4\u00e4j\u00e4 voisi selvitt\u00e4\u00e4, mitk\u00e4 s\u00e4hk\u00f6postiosoitteet ovat j\u00e4rjestelm\u00e4ss\u00e4. T\u00e4m\u00e4 on hienovarainen mutta t\u00e4rke\u00e4 tapa est\u00e4\u00e4 k\u00e4ytt\u00e4jien luettelointi.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-4\">Vaihe 4: Tokenien apufunktiot ja rekister\u00f6inti-endpoint<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Keskit\u00e4 tokenien luonti yhteen tiedostoon, jotta logiikkaa ei toisteta. Luo <code>tokens.js<\/code>, joka allekirjoittaa ja tarkistaa tokenit ymp\u00e4rist\u00f6muuttujista luetuilla avaimilla.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ tokens.js\nimport jwt from 'jsonwebtoken';\n\nconst ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;\nconst REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;\n\nexport function signAccessToken(user) {\n  return jwt.sign(\n    { sub: user.id, email: user.email },\n    ACCESS_SECRET,\n    { expiresIn: process.env.ACCESS_TOKEN_TTL, algorithm: 'HS256',\n      issuer: 'jwt-auth-api', audience: 'jwt-auth-client' }\n  );\n}\n\nexport function signRefreshToken(user, tokenId) {\n  return jwt.sign(\n    { sub: user.id, jti: tokenId },\n    REFRESH_SECRET,\n    { expiresIn: process.env.REFRESH_TOKEN_TTL, algorithm: 'HS256',\n      issuer: 'jwt-auth-api', audience: 'jwt-auth-client' }\n  );\n}\n\nexport function verifyAccessToken(token) {\n  return jwt.verify(token, ACCESS_SECRET,\n    { algorithms: ['HS256'], issuer: 'jwt-auth-api', audience: 'jwt-auth-client' });\n}\n\nexport function verifyRefreshToken(token) {\n  return jwt.verify(token, REFRESH_SECRET,\n    { algorithms: ['HS256'], issuer: 'jwt-auth-api', audience: 'jwt-auth-client' });\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Huomaa kaksi ratkaisevaa tietoturvayksityiskohtaa. Ensinn\u00e4kin <code>verify<\/code>-kutsuissa m\u00e4\u00e4ritet\u00e4\u00e4n <code>algorithms: ['HS256']<\/code> nimenomaisesti. T\u00e4m\u00e4 est\u00e4\u00e4 algoritmien sekaannushy\u00f6kk\u00e4yksen, jossa hy\u00f6kk\u00e4\u00e4j\u00e4 vaihtaa headerin algoritmiksi <code>none<\/code> tai yritt\u00e4\u00e4 RS256-to-HS256-temppua. Toiseksi tarkistamme aina <code>issuer<\/code>&#8211; ja <code>audience<\/code>-arvot, joten toisen palvelun tokenit hyl\u00e4t\u00e4\u00e4n.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Erilliset avaimet access- ja refresh-tokeneille ovat tarkoituksellinen valinta. Jos k\u00e4ytt\u00e4isit samaa avainta molempiin, refresh-tokenin vuoto vaarantaisi my\u00f6s access-tokenien tarkistuksen. Erottamalla avaimet rajaat vahingon: yhden avaimen paljastuminen ei automaattisesti murra koko j\u00e4rjestelm\u00e4\u00e4. Sama periaate koskee tuotantoymp\u00e4rist\u00f6j\u00e4, joissa kehitys-, testi- ja tuotantoavaimet pidet\u00e4\u00e4n tiukasti erill\u00e4\u00e4n. \u00c4l\u00e4 koskaan k\u00e4yt\u00e4 samaa avainta useassa ymp\u00e4rist\u00f6ss\u00e4, sill\u00e4 silloin kehitysavaimen vuoto altistaa tuotannon.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Luo nyt p\u00e4\u00e4sovellus <code>server.js<\/code> ja lis\u00e4\u00e4 rekister\u00f6inti-reitti.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js\nimport 'dotenv\/config';\nimport express from 'express';\nimport cookieParser from 'cookie-parser';\nimport { createUser, verifyUser } from '.\/store.js';\nimport {\n  signAccessToken, signRefreshToken,\n  verifyAccessToken, verifyRefreshToken\n} from '.\/tokens.js';\n\nconst app = express();\napp.use(express.json());\napp.use(cookieParser());\n\napp.post('\/auth\/register', async (req, res) =&gt; {\n  const { email, password } = req.body || {};\n  if (!email || !password || password.length &lt; 8) {\n    return res.status(400).json({ error: 'Sahkoposti ja vahintaan 8 merkin salasana vaaditaan' });\n  }\n  try {\n    const user = await createUser(email, password);\n    return res.status(201).json({ user });\n  } catch (e) {\n    return res.status(409).json({ error: e.message });\n  }\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () =&gt; console.log(`API kuuntelee portissa ${PORT}`));<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-5\">Vaihe 5: Kirjautuminen ja access-tokenin luonti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kirjautumisreitti tarkistaa tunnukset bcryptill\u00e4 ja palauttaa access-tokenin sek\u00e4 asettaa refresh-tokenin httpOnly-ev\u00e4steeseen. Pid\u00e4 access-token lyhytik\u00e4isen\u00e4, t\u00e4ss\u00e4 15 minuuttia. Lyhyt elinik\u00e4 rajaa vahingon, jos token vuotaa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lisaa server.js-tiedostoon, ennen app.listen-rivia\nimport crypto from 'crypto';\n\nconst refreshStore = new Map(); \/\/ jti -&gt; userId, korvaa tietokannalla\n\napp.post('\/auth\/login', async (req, res) =&gt; {\n  const { email, password } = req.body || {};\n  const user = await verifyUser(email, password);\n  if (!user) {\n    return res.status(401).json({ error: 'Virheellinen sahkoposti tai salasana' });\n  }\n  const tokenId = crypto.randomUUID();\n  refreshStore.set(tokenId, user.id);\n\n  const accessToken = signAccessToken(user);\n  const refreshToken = signRefreshToken(user, tokenId);\n\n  res.cookie('refreshToken', refreshToken, {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'strict',\n    path: '\/auth\/refresh',\n    maxAge: 7 * 24 * 60 * 60 * 1000\n  });\n  return res.json({ accessToken, user });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh-token tallennetaan palvelimen <code>refreshStore<\/code>-rakenteeseen avaimella <code>jti<\/code> (token-tunniste). T\u00e4m\u00e4 on t\u00e4rke\u00e4\u00e4: 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Huomaa ev\u00e4steen asetukset tarkasti. <code>httpOnly: true<\/code> est\u00e4\u00e4 JavaScripti\u00e4 lukemasta ev\u00e4stett\u00e4, mik\u00e4 suojaa XSS-varkaudelta. <code>secure<\/code> on p\u00e4\u00e4ll\u00e4 vain tuotannossa, koska kehityksess\u00e4 k\u00e4yt\u00e4t usein HTTP:t\u00e4 localhostissa. <code>sameSite: 'strict'<\/code> est\u00e4\u00e4 ev\u00e4steen l\u00e4htemisen muiden sivustojen pyynn\u00f6iss\u00e4, mik\u00e4 torjuu CSRF-hy\u00f6kk\u00e4ykset. <code>path: '\/auth\/refresh'<\/code> rajaa ev\u00e4steen l\u00e4htem\u00e4\u00e4n vain refresh-reitille, joten se ei kulje turhaan jokaisessa pyynn\u00f6ss\u00e4. N\u00e4m\u00e4 nelj\u00e4 asetusta yhdess\u00e4 muodostavat refresh-tokenin suojan perustan.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-6\">Vaihe 6: Todennus-middleware suojattuja reittej\u00e4 varten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Middleware lukee access-tokenin <code>Authorization<\/code>-otsakkeesta muodossa <code>Bearer &lt;token&gt;<\/code>, tarkistaa allekirjoituksen ja liitt\u00e4\u00e4 k\u00e4ytt\u00e4j\u00e4tiedot pyynt\u00f6\u00f6n. Jos token puuttuu tai on virheellinen, pyynt\u00f6 hyl\u00e4t\u00e4\u00e4n ennen kuin se p\u00e4\u00e4see suojattuun logiikkaan.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lisaa server.js-tiedostoon\nfunction requireAuth(req, res, next) {\n  const header = req.headers.authorization || '';\n  const [scheme, token] = header.split(' ');\n  if (scheme !== 'Bearer' || !token) {\n    return res.status(401).json({ error: 'Bearer-token puuttuu' });\n  }\n  try {\n    const payload = verifyAccessToken(token);\n    req.user = { id: payload.sub, email: payload.email };\n    next();\n  } catch (err) {\n    if (err.name === 'TokenExpiredError') {\n      return res.status(401).json({ error: 'Token on vanhentunut', code: 'TOKEN_EXPIRED' });\n    }\n    return res.status(401).json({ error: 'Virheellinen token' });\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Erottelemme vanhentuneen tokenin (<code>TokenExpiredError<\/code>) muista virheist\u00e4 omalla virhekoodilla. Selainpuoli voi k\u00e4ytt\u00e4\u00e4 koodia <code>TOKEN_EXPIRED<\/code> tunnistaakseen, milloin kannattaa pyyt\u00e4\u00e4 uutta access-tokenia refresh-tokenilla sen sijaan, ett\u00e4 k\u00e4ytt\u00e4j\u00e4 kirjataan ulos. Allekirjoituksen tarkistus nojaa samaan periaatteeseen kuin <a href=\"\/hmac-node-js\/\">HMAC-allekirjoitukset Node.js:ss\u00e4<\/a>, sill\u00e4 HS256 on k\u00e4yt\u00e4nn\u00f6ss\u00e4 HMAC-SHA256.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-7\">Vaihe 7: Suojattu reitti ja k\u00e4ytt\u00e4j\u00e4profiili<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nyt voit suojata mink\u00e4 tahansa reitin liitt\u00e4m\u00e4ll\u00e4 <code>requireAuth<\/code>-middlewaren. Lis\u00e4\u00e4 profiilireitti, joka palauttaa kirjautuneen k\u00e4ytt\u00e4j\u00e4n tiedot tokenista.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lisaa server.js-tiedostoon\napp.get('\/me', requireAuth, (req, res) =&gt; {\n  return res.json({\n    message: `Tervetuloa, ${req.user.email}`,\n    user: req.user\n  });\n});\n\n\/\/ Esimerkki rooliin perustuvasta suojauksesta\napp.get('\/admin', requireAuth, (req, res) =&gt; {\n  \/\/ Oikeudet tarkistetaan tietokannasta, ei tokenista, jotta muutokset\n  \/\/ astuvat voimaan heti eika vasta tokenin vanhennuttua\n  return res.json({ message: 'Vain todennetuille kayttajille' });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Huomaa kommentti admin-reitiss\u00e4. \u00c4l\u00e4 luota pelk\u00e4st\u00e4\u00e4n tokeniin upotettuun rooliin arkaluontoisissa oikeustarkistuksissa. Jos k\u00e4ytt\u00e4j\u00e4n oikeudet poistetaan, vanha token kantaa silti vanhaa roolia vanhentumiseensa asti. Kriittiset oikeudet kannattaa tarkistaa tietokannasta jokaisella pyynn\u00f6ll\u00e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4m\u00e4 erottelu authentikaation ja autorisaation v\u00e4lill\u00e4 on yksi yleisimmist\u00e4 v\u00e4\u00e4rink\u00e4sityksist\u00e4. JWT todistaa, ett\u00e4 token on aito ja kenelle se kuuluu (autentikaatio). Se ei automaattisesti kerro, mit\u00e4 k\u00e4ytt\u00e4j\u00e4 saa tehd\u00e4 (autorisaatio). P\u00e4\u00e4synhallinta on aina sovelluksen vastuulla, ja se kannattaa toteuttaa erillisen\u00e4 kerroksena todennuksen p\u00e4\u00e4lle. Pieni rooli kuten &#8220;lukija&#8221; voi olla tokenissa nopeuden vuoksi, mutta esimerkiksi maksun hyv\u00e4ksynt\u00e4 tai k\u00e4ytt\u00e4jien poisto pit\u00e4\u00e4 aina varmentaa tuoreesta tietol\u00e4hteest\u00e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-8\">Vaihe 8: Refresh-tokenit ja kierto (rotation)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh-token antaa uuden access-tokenin ilman uudelleenkirjautumista. Toteutamme kierron (rotation): jokainen refresh-pyynt\u00f6 mit\u00e4t\u00f6i vanhan refresh-tokenin ja luo uuden. Jos vanhaa, jo k\u00e4ytetty\u00e4 tokenia yritet\u00e4\u00e4n k\u00e4ytt\u00e4\u00e4 uudelleen, se on merkki varkaudesta ja koko token-perhe mit\u00e4t\u00f6id\u00e4\u00e4n.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lisaa server.js-tiedostoon\napp.post('\/auth\/refresh', (req, res) =&gt; {\n  const token = req.cookies?.refreshToken;\n  if (!token) {\n    return res.status(401).json({ error: 'Refresh-token puuttuu' });\n  }\n  let payload;\n  try {\n    payload = verifyRefreshToken(token);\n  } catch {\n    return res.status(401).json({ error: 'Virheellinen refresh-token' });\n  }\n\n  \/\/ Kierron tarkistus: onko tama jti yha voimassa?\n  if (!refreshStore.has(payload.jti)) {\n    \/\/ Uudelleenkaytto havaittu, mitatoi kaikki kayttajan tokenit\n    for (const [jti, uid] of refreshStore) {\n      if (uid === payload.sub) refreshStore.delete(jti);\n    }\n    return res.status(401).json({ error: 'Token mitatoity, kirjaudu uudelleen' });\n  }\n\n  \/\/ Mitatoi vanha, luo uusi\n  refreshStore.delete(payload.jti);\n  const newId = crypto.randomUUID();\n  refreshStore.set(newId, payload.sub);\n\n  const user = { id: payload.sub };\n  const accessToken = signAccessToken(user);\n  const refreshToken = signRefreshToken(user, newId);\n\n  res.cookie('refreshToken', refreshToken, {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'strict',\n    path: '\/auth\/refresh',\n    maxAge: 7 * 24 * 60 * 60 * 1000\n  });\n  return res.json({ accessToken });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kierto on OWASP:n suosittelema kontrolli. Se muuttaa varastetun refresh-tokenin l\u00e4hes hy\u00f6dytt\u00f6m\u00e4ksi: heti kun joko oikea k\u00e4ytt\u00e4j\u00e4 tai hy\u00f6kk\u00e4\u00e4j\u00e4 k\u00e4ytt\u00e4\u00e4 tokenin, toinen osapuoli huomaa uudelleenk\u00e4yt\u00f6n ja koko ketju katkeaa. Tuotannossa tallenna <code>jti<\/code>-tunnisteet tietokantaan, ei muistiin, jotta ne s\u00e4ilyv\u00e4t palvelimen uudelleenk\u00e4ynnistyksen yli.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-9\">Vaihe 9: httpOnly-ev\u00e4steet vai localStorage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Yksi yleisimmist\u00e4 kysymyksist\u00e4 on, mihin token tallennetaan selaimessa. Vaihtoehtoja on kaksi, ja niill\u00e4 on erilaiset uhkamallit. T\u00e4m\u00e4 taulukko vertaa niit\u00e4.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Ominaisuus<\/th><th>httpOnly-ev\u00e4ste<\/th><th>localStorage<\/th><\/tr><\/thead><tbody>\n<tr><td>JavaScript p\u00e4\u00e4see tokeniin<\/td><td>Ei (suojaa XSS:lt\u00e4)<\/td><td>Kyll\u00e4 (altis XSS-varkaudelle)<\/td><\/tr>\n<tr><td>L\u00e4htee automaattisesti pyynn\u00f6iss\u00e4<\/td><td>Kyll\u00e4<\/td><td>Ei, liitett\u00e4v\u00e4 k\u00e4sin<\/td><\/tr>\n<tr><td>CSRF-riski<\/td><td>On, vaatii SameSite-suojan<\/td><td>Ei<\/td><\/tr>\n<tr><td>Toimii mobiilisovelluksissa<\/td><td>Hankalasti<\/td><td>Helposti<\/td><\/tr>\n<tr><td>Suositus 2026<\/td><td>Refresh-tokenille<\/td><td>V\u00e4lt\u00e4 arkaluontoisille tokeneille<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4ss\u00e4 oppaassa k\u00e4ytet\u00e4\u00e4n hybridimallia, jota pidet\u00e4\u00e4n parhaana k\u00e4yt\u00e4nt\u00f6n\u00e4 2026: pitk\u00e4ik\u00e4inen refresh-token on httpOnly-ev\u00e4steess\u00e4 (JavaScript ei p\u00e4\u00e4se siihen k\u00e4siksi, joten XSS ei voi varastaa sit\u00e4), ja lyhytik\u00e4inen access-token pidet\u00e4\u00e4n selaimen muistissa. Koska access-token vanhenee 15 minuutissa, sen vuotamisen vahinko on rajattu. Yhdist\u00e4 t\u00e4m\u00e4 aina HTTPS-yhteyteen; lue lis\u00e4\u00e4 <a href=\"\/https-ja-tls\/\">HTTPS:st\u00e4 ja TLS:st\u00e4<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Koska k\u00e4yt\u00e4mme ev\u00e4stett\u00e4, <code>sameSite: 'strict'<\/code> suojaa CSRF-hy\u00f6kk\u00e4yksilt\u00e4 rajaamalla ev\u00e4steen l\u00e4htem\u00e4\u00e4n vain saman sivuston pyynn\u00f6iss\u00e4. T\u00e4m\u00e4 on t\u00e4rke\u00e4 yksityiskohta, jonka moni ohittaa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-10\">Vaihe 10: Uloskirjautuminen ja tokenin mit\u00e4t\u00f6inti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Uloskirjautuminen poistaa refresh-tokenin sek\u00e4 varastosta ett\u00e4 selaimen ev\u00e4steest\u00e4. Access-token ei katoa heti, mutta se vanhenee itsest\u00e4\u00e4n 15 minuutissa, ja koska refresh-token on mit\u00e4t\u00f6ity, uutta access-tokenia ei voi en\u00e4\u00e4 luoda.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lisaa server.js-tiedostoon\napp.post('\/auth\/logout', (req, res) =&gt; {\n  const token = req.cookies?.refreshToken;\n  if (token) {\n    try {\n      const payload = verifyRefreshToken(token);\n      refreshStore.delete(payload.jti);\n    } catch {\n      \/\/ virheellinen token, ei tarvitse tehda mitaan\n    }\n  }\n  res.clearCookie('refreshToken', { path: '\/auth\/refresh' });\n  return res.json({ message: 'Uloskirjautuminen onnistui' });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Jos tarvitset v\u00e4lit\u00f6nt\u00e4 access-tokenien mit\u00e4t\u00f6inti\u00e4 (esimerkiksi pankkisovelluksessa), pid\u00e4 palvelimella mustaa listaa mit\u00e4t\u00f6idyist\u00e4 token-tunnisteista ja tarkista se <code>requireAuth<\/code>-middlewaressa. T\u00e4m\u00e4 tuo takaisin osan tilallisuudesta, mutta on joskus v\u00e4ltt\u00e4m\u00e4t\u00f6nt\u00e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4yt\u00e4nn\u00f6ss\u00e4 musta lista kannattaa toteuttaa nopealla muistivarastolla kuten Redisill\u00e4, jossa jokainen mit\u00e4t\u00f6ity token-tunniste s\u00e4ilyy vain tokenin j\u00e4ljell\u00e4 olevan elini\u00e4n ajan. Koska access-token vanhenee 15 minuutissa, mustalla listalla on enimmill\u00e4\u00e4n 15 minuutin verran tunnisteita kerrallaan, joten se pysyy pienen\u00e4. T\u00e4m\u00e4 on hyv\u00e4 kompromissi: s\u00e4ilyt\u00e4t JWT:n nopeuden valtaosassa pyynt\u00f6j\u00e4, mutta saat v\u00e4litt\u00f6m\u00e4n mit\u00e4t\u00f6innin silloin kun sit\u00e4 todella tarvitset.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-11\">Vaihe 11: Allekirjoitusalgoritmit, HS256 vs RS256 vs EdDSA<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4h\u00e4n asti olemme k\u00e4ytt\u00e4neet HS256:ta, joka k\u00e4ytt\u00e4\u00e4 yht\u00e4 jaettua salaisuutta sek\u00e4 allekirjoitukseen ett\u00e4 tarkistukseen. Se on yksinkertainen ja nopea, kun sama palvelin tekee molemmat. Jos taas eri palvelu allekirjoittaa ja toinen tarkistaa, tarvitset ep\u00e4symmetrist\u00e4 algoritmia, jossa yksityinen avain allekirjoittaa ja julkinen avain tarkistaa.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Algoritmi<\/th><th>Tyyppi<\/th><th>Avain<\/th><th>Paras k\u00e4ytt\u00f6tapaus<\/th><\/tr><\/thead><tbody>\n<tr><td>HS256<\/td><td>Symmetrinen (HMAC-SHA256)<\/td><td>Yksi jaettu salaisuus<\/td><td>Yksi backend allekirjoittaa ja tarkistaa<\/td><\/tr>\n<tr><td>RS256<\/td><td>Ep\u00e4symmetrinen (RSA)<\/td><td>Yksityinen + julkinen<\/td><td>Issuer ja verifier eri palveluissa<\/td><\/tr>\n<tr><td>ES256<\/td><td>Ep\u00e4symmetrinen (ECDSA P-256)<\/td><td>Yksityinen + julkinen<\/td><td>Pienemm\u00e4t avaimet, mobiili<\/td><\/tr>\n<tr><td>EdDSA<\/td><td>Ep\u00e4symmetrinen (Ed25519)<\/td><td>Yksityinen + julkinen<\/td><td>Moderni, nopea, suositeltu uusiin<\/td><\/tr>\n<tr><td>none<\/td><td>Ei allekirjoitusta<\/td><td>Ei mit\u00e4\u00e4n<\/td><td>\u00c4l\u00e4 koskaan k\u00e4yt\u00e4, vakava aukko<\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e4in vaihdat RS256:een. Generoi avainpari OpenSSL:ll\u00e4, allekirjoita yksityisell\u00e4 avaimella ja tarkista julkisella. Julkisen avaimen voi jakaa vapaasti palveluille, jotka tarkistavat tokeneita.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Generoi RSA-avainpari\nopenssl genrsa -out private.pem 2048\nopenssl rsa -in private.pem -pubout -out public.pem<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ RS256-allekirjoitus ja tarkistus\nimport fs from 'fs';\nimport jwt from 'jsonwebtoken';\n\nconst privateKey = fs.readFileSync('private.pem');\nconst publicKey = fs.readFileSync('public.pem');\n\nconst token = jwt.sign({ sub: '1' }, privateKey, {\n  algorithm: 'RS256', expiresIn: '15m'\n});\n\n\/\/ Tarkistuksessa salli VAIN RS256, esta algoritmien sekaannus\nconst payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ratkaiseva s\u00e4\u00e4nt\u00f6: tarkistuksessa m\u00e4\u00e4rit\u00e4 aina sallitut algoritmit nimenomaisesti (<code>algorithms: ['RS256']<\/code>). Jos j\u00e4t\u00e4t t\u00e4m\u00e4n pois, hy\u00f6kk\u00e4\u00e4j\u00e4 voi yritt\u00e4\u00e4 sy\u00f6tt\u00e4\u00e4 julkisen avaimen HMAC-salaisuutena ja v\u00e4\u00e4rent\u00e4\u00e4 tokeneita. T\u00e4m\u00e4 RS256-to-HS256-sekaannus on yksi tunnetuimmista JWT-aukoista. JWT:n ep\u00e4symmetrinen allekirjoitus toimii samalla periaatteella kuin <a href=\"\/digitaaliset-allekirjoitukset\/\">digitaaliset allekirjoitukset<\/a> yleisesti.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-12\">Vaihe 12: Testaus curlilla ja vienti tuotantoon<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4ynnist\u00e4 palvelin komennolla <code>node server.js<\/code> ja testaa koko kierto curlilla. Ensin rekister\u00f6inti, sitten kirjautuminen, sitten suojattu reitti.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># 1. Rekisteroi kayttaja\ncurl -s -X POST http:\/\/localhost:3000\/auth\/register \\\n  -H 'Content-Type: application\/json' \\\n  -d '{\"email\":\"matti@example.fi\",\"password\":\"vahvaSalasana123\"}'\n\n# 2. Kirjaudu sisaan, talleta evaste ja access-token\ncurl -s -X POST http:\/\/localhost:3000\/auth\/login \\\n  -H 'Content-Type: application\/json' \\\n  -c cookies.txt \\\n  -d '{\"email\":\"matti@example.fi\",\"password\":\"vahvaSalasana123\"}'\n\n# 3. Kayta access-tokenia suojattuun reittiin\nTOKEN=\"liita_saamasi_accessToken_tahan\"\ncurl -s http:\/\/localhost:3000\/me \\\n  -H \"Authorization: Bearer $TOKEN\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Odotettu tuloste kohdasta 3\n{\"message\":\"Tervetuloa, matti@example.fi\",\"user\":{\"id\":\"1\",\"email\":\"matti@example.fi\"}}\n\n# Refresh-pyynto evasteella\ncurl -s -X POST http:\/\/localhost:3000\/auth\/refresh -b cookies.txt\n{\"accessToken\":\"eyJhbGciOiJIUzI1NiI...\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kun kaikki vaiheet toimivat, sinulla on t\u00e4ysi JWT-todennusj\u00e4rjestelm\u00e4. Ennen tuotantoon vienti\u00e4 varmista, ett\u00e4 <code>NODE_ENV=production<\/code> on asetettu (jolloin ev\u00e4steet k\u00e4ytt\u00e4v\u00e4t <code>secure<\/code>-lippua), palvelin on HTTPS:n takana, refresh-tokenit tallennetaan tietokantaan ja avaimet tulevat salaisuuksien hallinnasta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"yleiset-virheet\">JWT-tietoturva: 5 yleist\u00e4 virhett\u00e4, jotka pit\u00e4\u00e4 v\u00e4ltt\u00e4\u00e4<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Suurin osa JWT-haavoittuvuuksista syntyy samoista perusvirheist\u00e4. OWASP:n JWT-ohjeistus nostaa esiin n\u00e4m\u00e4 toistuvasti.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Algoritmin j\u00e4tt\u00e4minen tarkistamatta.<\/strong> Jos et anna <code>verify<\/code>-kutsulle <code>algorithms<\/code>-listaa, kirjasto saattaa hyv\u00e4ksy\u00e4 headerin ilmoittaman algoritmin. Hy\u00f6kk\u00e4\u00e4j\u00e4 voi asettaa <code>alg: none<\/code> tai vaihtaa RS256:n HS256:een. Salli aina vain yksi tunnettu algoritmi.<\/li>\n<li><strong>Arkaluontoinen tieto payloadissa.<\/strong> Payload on koodattu, ei salattu. Salasanat, henkil\u00f6tunnukset tai luottokorttitiedot tokenissa vuotavat kaikille, jotka n\u00e4kev\u00e4t tokenin.<\/li>\n<li><strong>Liian pitk\u00e4 access-tokenin elinik\u00e4.<\/strong> Tunteja tai p\u00e4ivi\u00e4 kest\u00e4v\u00e4 access-token tarkoittaa, ett\u00e4 varastetulla tokenilla voi tehd\u00e4 vahinkoa pitk\u00e4\u00e4n. Pid\u00e4 se 15 minuutissa ja k\u00e4yt\u00e4 refresh-tokenia.<\/li>\n<li><strong>Heikko tai kovakoodattu allekirjoitusavain.<\/strong> Lyhyt avain on murrettavissa raa&#8217;alla voimalla. L\u00e4hdekoodiin kovakoodattu avain vuotaa heti, kun repositorio jaetaan. K\u00e4yt\u00e4 256-bittist\u00e4 satunnaisavainta ymp\u00e4rist\u00f6muuttujassa.<\/li>\n<li><strong>Tokenin tallennus localStorageen ilman XSS-suojaa.<\/strong> Jos sivustolla on XSS-aukko, hy\u00f6kk\u00e4\u00e4j\u00e4n skripti lukee localStoragessa olevan tokenin. K\u00e4yt\u00e4 httpOnly-ev\u00e4stett\u00e4 refresh-tokenille.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vianetsinta\">Vianetsint\u00e4: 8 yleist\u00e4 ongelmaa ja ratkaisut<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Alla tavallisimmat virheilmoitukset, joihin t\u00f6rm\u00e4\u00e4t, ja niiden syyt.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Virhe \/ oire<\/th><th>Syy<\/th><th>Ratkaisu<\/th><\/tr><\/thead><tbody>\n<tr><td><code>invalid signature<\/code><\/td><td>Allekirjoitukseen ja tarkistukseen eri avain<\/td><td>Varmista, ett\u00e4 sama <code>JWT_ACCESS_SECRET<\/code> on k\u00e4yt\u00f6ss\u00e4<\/td><\/tr>\n<tr><td><code>jwt expired<\/code><\/td><td>Access-token vanheni<\/td><td>Pyyd\u00e4 uusi token <code>\/auth\/refresh<\/code>-reitilt\u00e4<\/td><\/tr>\n<tr><td><code>jwt malformed<\/code><\/td><td>Token ei ole kolmiosainen tai on katkennut<\/td><td>Tarkista, ett\u00e4 <code>Bearer<\/code>-etuliite ja token erotellaan oikein<\/td><\/tr>\n<tr><td><code>secretOrPrivateKey must have a value<\/code><\/td><td><code>.env<\/code> ei latautunut<\/td><td>Varmista <code>import 'dotenv\/config'<\/code> ennen muuta koodia<\/td><\/tr>\n<tr><td><code>jwt audience invalid<\/code><\/td><td><code>audience<\/code> ei t\u00e4sm\u00e4\u00e4<\/td><td>K\u00e4yt\u00e4 samaa arvoa allekirjoituksessa ja tarkistuksessa<\/td><\/tr>\n<tr><td>Ev\u00e4ste ei tallennu selaimeen<\/td><td><code>secure: true<\/code> ilman HTTPS:\u00e4\u00e4<\/td><td>Kehityksess\u00e4 pid\u00e4 <code>NODE_ENV=development<\/code><\/td><\/tr>\n<tr><td><code>req.body<\/code> on <code>undefined<\/code><\/td><td><code>express.json()<\/code> puuttuu<\/td><td>Lis\u00e4\u00e4 <code>app.use(express.json())<\/code><\/td><\/tr>\n<tr><td>401 heti kirjautumisen j\u00e4lkeen<\/td><td>Kellojen ero palvelimien v\u00e4lill\u00e4<\/td><td>Synkronoi NTP:ll\u00e4 tai salli pieni <code>clockTolerance<\/code><\/td><\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Jos n\u00e4et <code>invalid signature<\/code> -virheen vaikka avaimet n\u00e4ytt\u00e4v\u00e4t oikeilta, tarkista ettei <code>.env<\/code>-tiedostoon ole j\u00e4\u00e4nyt rivinvaihtoa tai lainausmerkkej\u00e4 avaimen ymp\u00e4rille. <code>dotenv<\/code> lukee arvon kirjaimellisesti. Yleinen ansa on my\u00f6s, ett\u00e4 kehitysymp\u00e4rist\u00f6ss\u00e4 ajetaan vahingossa kahta palvelinta eri avaimilla, jolloin yhdess\u00e4 luotu token ei kelpaa toisessa. Pys\u00e4yt\u00e4 kaikki <code>node<\/code>-prosessit ja k\u00e4ynnist\u00e4 palvelin uudelleen, jos ep\u00e4ilet t\u00e4t\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"virheiden-loki\">Virheiden lokitus turvallisesti<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Kun tutkit todennusvirheit\u00e4, \u00e4l\u00e4 koskaan tulosta koko tokenia lokiin. Token on k\u00e4yt\u00e4nn\u00f6ss\u00e4 salasana, ja lokit p\u00e4\u00e4tyv\u00e4t usein paikkoihin, joissa niit\u00e4 lukee moni. Lokita sen sijaan vain virhetyyppi ja tarvittaessa tokenin <code>jti<\/code>-tunniste, jonka avulla voit j\u00e4ljitt\u00e4\u00e4 tapahtuman ilman, ett\u00e4 itse token vuotaa. T\u00e4m\u00e4 on pieni mutta t\u00e4rke\u00e4 tapa, joka erottaa kokeneen kehitt\u00e4j\u00e4n aloittelijasta.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Turvallinen lokitus todennusvirheessa\ncatch (err) {\n  console.warn('Todennus epaonnistui:', err.name); \/\/ EI koko tokenia\n  return res.status(401).json({ error: 'Virheellinen token' });\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cve-historia\">jsonwebtoken-kirjaston haavoittuvuushistoria<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kirjaston historia osoittaa, miksi versioiden ajan tasalla pit\u00e4minen on t\u00e4rke\u00e4\u00e4. 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\u00e4t neuvonnot CVE-2022-23539 ja CVE-2022-23540. Vanhoissa 8.x-versioissa n\u00e4m\u00e4 j\u00e4iv\u00e4t korjaamatta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4yt\u00e4 siksi aina v\u00e4hint\u00e4\u00e4n versiota 9.0.0, mieluiten uusinta korjattua julkaisua (t\u00e4t\u00e4 kirjoitettaessa 9.0.3). \u00c4l\u00e4 koskaan kirjoita omaa JWT-purkulogiikkaa: virheet allekirjoituksen tarkistuksessa ovat helppoja tehd\u00e4 ja vaikeita huomata. Voit seurata kirjaston julkaisuja sen <a href=\"https:\/\/github.com\/auth0\/node-jsonwebtoken\" target=\"_blank\" rel=\"noopener nofollow\">GitHub-sivulla<\/a> ja lukea OWASP:n suositukset <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/JSON_Web_Token_for_Java_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener nofollow\">JWT-tietoturvasta<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"suorituskyky\">Suorituskyky ja skaalautuvuus tuotannossa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JWT:n suurin suorituskykyetu on, ett\u00e4 access-tokenin tarkistus on pelkk\u00e4 HMAC-laskenta ilman tietokantahakua. Yksi palvelinydin tarkistaa tuhansia HS256-tokeneita sekunnissa. T\u00e4m\u00e4 tekee JWT:st\u00e4 erinomaisen valinnan rajapinnoille, joissa pyynt\u00f6j\u00e4 tulee paljon ja jokainen niist\u00e4 pit\u00e4\u00e4 todentaa. Vertailun vuoksi: palvelinistunto vaatii usein hakuun Redis- tai tietokantakutsun, joka lis\u00e4\u00e4 muutaman millisekunnin jokaiseen pyynt\u00f6\u00f6n.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Algoritmin valinta vaikuttaa nopeuteen. HS256 (symmetrinen) on selv\u00e4sti nopein sek\u00e4 allekirjoituksessa ett\u00e4 tarkistuksessa. RS256 (RSA) on raskaampi, erityisesti allekirjoituksessa, koska RSA-laskenta on hidasta. EdDSA (Ed25519) tarjoaa ep\u00e4symmetrisen allekirjoituksen l\u00e4hes symmetrisen nopeudella ja pienemmill\u00e4 avaimilla, mink\u00e4 vuoksi se on suositeltava valinta uusiin ep\u00e4symmetrisiin j\u00e4rjestelmiin. Jos rajapintaasi tulee kymmeni\u00e4 tuhansia pyynt\u00f6j\u00e4 sekunnissa, algoritmin valinta alkaa n\u00e4ky\u00e4 prosessorikuormassa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pid\u00e4 tokenit pienin\u00e4. Jokainen claim kasvattaa tokenia, ja token kulkee jokaisen pyynn\u00f6n mukana otsakkeessa. Suuri token sy\u00f6 kaistaa ja hidastaa pyynt\u00f6j\u00e4. Laita tokeniin vain v\u00e4ltt\u00e4m\u00e4t\u00f6n: k\u00e4ytt\u00e4j\u00e4n tunniste, vanhentumisaika ja ehk\u00e4 rooli. Hae kaikki muu tarvittava tieto tietokannasta tunnisteen perusteella. T\u00e4m\u00e4 my\u00f6s pienent\u00e4\u00e4 riski\u00e4, ett\u00e4 arkaluontoista tietoa vuotaa koodatun payloadin kautta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"edistyneet-vinkit\">Edistyneet vinkit tuotantok\u00e4ytt\u00f6\u00f6n<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"avainten-kierto\">Avainten kierto ja kid-otsake<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tuotannossa allekirjoitusavain kannattaa vaihtaa s\u00e4\u00e4nn\u00f6llisesti. Lis\u00e4\u00e4 tokenin headeriin <code>kid<\/code>-tunniste (key ID), joka kertoo, mill\u00e4 avaimella token allekirjoitettiin. N\u00e4in voit pit\u00e4\u00e4 useaa avainta voimassa siirtym\u00e4kauden ajan ja tarkistaa kunkin tokenin oikealla avaimella. T\u00e4m\u00e4 mahdollistaa avainten vaihdon ilman, ett\u00e4 kaikki k\u00e4ytt\u00e4j\u00e4t kirjautuvat ulos kerralla.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"rate-limiting\">Pyynt\u00f6rajoitus kirjautumiseen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Suojaa kirjautumisreitti raa&#8217;alta l\u00e4pik\u00e4ynnilt\u00e4 pyynt\u00f6rajoituksella. Esimerkiksi <code>express-rate-limit<\/code> rajaa yhden IP:n yritykset esimerkiksi viiteen viiden minuutin aikana. Yhdist\u00e4 t\u00e4m\u00e4 bcryptin hitauteen, niin salasanojen arvaaminen muuttuu ep\u00e4k\u00e4yt\u00e4nn\u00f6lliseksi. T\u00e4m\u00e4 on t\u00e4rke\u00e4 lis\u00e4, sill\u00e4 JWT ei itsess\u00e4\u00e4n suojaa heikoilta salasanoilta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"clock-tolerance\">Kellotoleranssi hajautetuissa j\u00e4rjestelmiss\u00e4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jos palvelimien kellot eroavat hieman, juuri luotu token voi n\u00e4ytt\u00e4\u00e4 vanhentuneelta tai viel\u00e4 voimaan tulemattomalta. Anna <code>verify<\/code>-kutsulle <code>clockTolerance: 5<\/code> (sekuntia), niin pieni ero ei kaada todennusta. Pid\u00e4 toleranssi kuitenkin pienen\u00e4, sill\u00e4 liian suuri arvo heikent\u00e4\u00e4 <code>exp<\/code>-tarkistuksen tehoa. Paras ratkaisu on synkronoida kaikkien palvelimien kellot NTP:ll\u00e4, jolloin toleranssia ei juuri tarvita.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mobiili-tallennus\">Tokenin tallennus mobiilisovelluksessa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Mobiilisovelluksissa httpOnly-ev\u00e4steet eiv\u00e4t toimi yht\u00e4 luontevasti kuin selaimessa, joten token tallennetaan yleens\u00e4 laitteen suojattuun avainvarastoon. iOS:ss\u00e4 t\u00e4m\u00e4 on Keychain ja Androidissa EncryptedSharedPreferences tai Keystore. \u00c4l\u00e4 koskaan tallenna tokenia tavalliseen tekstitiedostoon tai suojaamattomaan asetusvarastoon. Suojattu avainvarasto salaa tokenin laitteen laitteistopohjaisella avaimella, joten muut sovellukset eiv\u00e4t p\u00e4\u00e4se siihen k\u00e4siksi edes silloin, kun laite on juurrutettu tai jailbreikattu.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"claims-suunnittelu\">Claimien suunnittelu<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Hyv\u00e4 token sis\u00e4lt\u00e4\u00e4 standardiclaimit, joita kirjastot ymm\u00e4rt\u00e4v\u00e4t: <code>sub<\/code> (subjekti eli k\u00e4ytt\u00e4j\u00e4), <code>iat<\/code> (luontiaika), <code>exp<\/code> (vanhentuminen), <code>iss<\/code> (my\u00f6nt\u00e4j\u00e4), <code>aud<\/code> (vastaanottaja) ja <code>jti<\/code> (yksil\u00f6llinen tunniste). Omat claimit kannattaa nimet\u00e4 kuvaavasti ja pit\u00e4\u00e4 lyhyin\u00e4. V\u00e4lt\u00e4 koko k\u00e4ytt\u00e4j\u00e4objektin ahtamista tokeniin. Mit\u00e4 v\u00e4hemm\u00e4n token sis\u00e4lt\u00e4\u00e4, sit\u00e4 pienempi se on ja sit\u00e4 v\u00e4hemm\u00e4n tietoa vuotaa, jos token p\u00e4\u00e4tyy v\u00e4\u00e4riin k\u00e4siin. Kun lis\u00e4\u00e4t rooleja tai oikeuksia tokeniin, muista ett\u00e4 ne j\u00e4\u00e4tyv\u00e4t tokenin luontihetkeen, joten arkaluontoiset oikeudet kannattaa silti tarkistaa tietokannasta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"usein-kysytyt\">Usein kysytyt kysymykset<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"onko-jwt-turvallinen-istuntojen-korvaaja\">Onko JWT turvallinen istuntojen korvaaja?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">JWT on turvallinen, kun se toteutetaan oikein: lyhyt access-tokenin elinik\u00e4, refresh-tokenien kierto, allekirjoitusalgoritmin nimenomainen tarkistus ja HTTPS. JWT ei kuitenkaan ole automaattisesti parempi kuin palvelinpuolen istunto. Jos tarvitset v\u00e4lit\u00f6nt\u00e4 uloskirjautumista kaikilta laitteilta, tilalliset istunnot voivat olla yksinkertaisempia.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mihin-token-kannattaa-tallentaa-selaimessa\">Mihin token kannattaa tallentaa selaimessa?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh-token kannattaa pit\u00e4\u00e4 httpOnly-ev\u00e4steess\u00e4, johon JavaScript ei p\u00e4\u00e4se k\u00e4siksi, joten XSS ei voi varastaa sit\u00e4. Lyhytik\u00e4inen access-token voidaan pit\u00e4\u00e4 selaimen muistissa. V\u00e4lt\u00e4 pitk\u00e4ik\u00e4isten tokenien tallentamista localStorageen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kuinka-pitka-access-tokenin-elinian-pitaisi-olla\">Kuinka pitk\u00e4 access-tokenin elini\u00e4n pit\u00e4isi olla?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yleinen suositus 2026 on 15 minuuttia. Lyhyt elinik\u00e4 rajaa varastetun tokenin vahingon, ja refresh-token huolehtii siit\u00e4, ettei k\u00e4ytt\u00e4j\u00e4n tarvitse kirjautua uudelleen kovin usein. Refresh-token voi kest\u00e4\u00e4 esimerkiksi 7 p\u00e4iv\u00e4\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"voiko-jwtn-kumota-ennen-vanhentumista\">Voiko JWT:n kumota ennen vanhentumista?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pelkk\u00e4\u00e4 tilatonta access-tokenia ei voi suoraan kumota, mutta refresh-tokenit voi mit\u00e4t\u00f6id\u00e4 poistamalla niiden tunnisteet palvelimelta. Jos tarvitset v\u00e4lit\u00f6nt\u00e4 access-tokenin mit\u00e4t\u00f6inti\u00e4, yll\u00e4pid\u00e4 palvelimella mustaa listaa mit\u00e4t\u00f6idyist\u00e4 token-tunnisteista ja tarkista se jokaisella pyynn\u00f6ll\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hs256-vai-rs256\">HS256 vai RS256?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4yt\u00e4 HS256:ta, kun sama backend sek\u00e4 allekirjoittaa ett\u00e4 tarkistaa tokenit. Valitse RS256 tai EdDSA, kun eri palvelut hoitavat allekirjoituksen ja tarkistuksen, koska ep\u00e4symmetrisess\u00e4 mallissa julkisen avaimen voi jakaa turvallisesti ilman, ett\u00e4 se vaarantaa allekirjoituskyky\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"tarvitseeko-jwt-tietokantaa\">Tarvitseeko JWT tietokantaa?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Access-tokenien tarkistus ei vaadi tietokantaa, mik\u00e4 on JWT:n p\u00e4\u00e4etu. Refresh-tokenien kierto ja mit\u00e4t\u00f6inti sen sijaan kannattaa tukea tietokannalla tuotannossa, jotta tilatieto s\u00e4ilyy palvelimen uudelleenk\u00e4ynnistyksen yli ja skaalautuu usealle palvelimelle.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"mita-algnone-tarkoittaa-ja-miksi-se-on-vaarallinen\">Mit\u00e4 alg:none tarkoittaa ja miksi se on vaarallinen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Arvo <code>alg: none<\/code> kertoo, ett\u00e4 token ei ole allekirjoitettu. Jos palvelin hyv\u00e4ksyy t\u00e4m\u00e4n, kuka tahansa voi luoda v\u00e4\u00e4rennetyn tokenin mill\u00e4 tahansa sis\u00e4ll\u00f6ll\u00e4. Est\u00e4 se aina m\u00e4\u00e4ritt\u00e4m\u00e4ll\u00e4 sallitut algoritmit <code>verify<\/code>-kutsussa, jolloin allekirjoittamattomat tokenit hyl\u00e4t\u00e4\u00e4n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"toimiiko-tama-express-4lla\">Toimiiko t\u00e4m\u00e4 Express 4:ll\u00e4?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Periaate on sama, mutta Express 5 muutti muutamia oletuksia, kuten asynkronisten virheiden k\u00e4sittely\u00e4. T\u00e4m\u00e4n oppaan koodi on testattu Express 5.2.1:ll\u00e4. Express 4:ll\u00e4 koodi toimii pienin muutoksin, mutta uusiin projekteihin kannattaa valita Express 5.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"yhteenveto\">Yhteenveto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rakensit t\u00e4yden JWT-todennusj\u00e4rjestelm\u00e4n Node.js:ll\u00e4 ja Express 5:ll\u00e4: rekister\u00f6inti bcrypt-tiivisteill\u00e4, kirjautuminen, lyhytik\u00e4iset access-tokenit, httpOnly-ev\u00e4steess\u00e4 kulkevat refresh-tokenit ja niiden kierto. T\u00e4rkeimm\u00e4t tietoturvaperiaatteet olivat allekirjoitusalgoritmin nimenomainen tarkistus, vahva satunnaisavain, lyhyt tokenin elinik\u00e4 ja salaisuuksien pit\u00e4minen poissa l\u00e4hdekoodista. N\u00e4m\u00e4 nelj\u00e4 asiaa erottavat tuotantokelpoisen toteutuksen aukollisesta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Voit laajentaa projektia korvaamalla muistinvaraiset varastot tietokannalla, lis\u00e4\u00e4m\u00e4ll\u00e4 pyynt\u00f6rajoituksen ja siirtym\u00e4ll\u00e4 ep\u00e4symmetriseen allekirjoitukseen mikropalveluissa. JWT:n perusta pysyy samana riippumatta siit\u00e4, kuinka suureksi j\u00e4rjestelm\u00e4si kasvaa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Muista lopuksi, ett\u00e4 tokeniin liittyv\u00e4 tietoturva on ketju, jonka heikoin lenkki ratkaisee. Vahva avain ei auta, jos token kulkee salaamattomana HTTP:n yli. Lyhyt elinik\u00e4 ei auta, jos refresh-token vuotaa localStoragesta XSS-aukon kautta. Algoritmin tarkistus ei auta, jos avain on kovakoodattu repositorioon. K\u00e4y t\u00e4m\u00e4n oppaan tarkistuslista l\u00e4pi ennen jokaista tuotantojulkaisua: vahva satunnaisavain ymp\u00e4rist\u00f6muuttujassa, nimenomainen algoritmin tarkistus, lyhyt access-tokenin elinik\u00e4, refresh-tokenin kierto, httpOnly-ev\u00e4ste ja HTTPS kaiken p\u00e4\u00e4ll\u00e4. Kun n\u00e4m\u00e4 kuusi asiaa ovat kunnossa, JWT-toteutuksesi kest\u00e4\u00e4 valtaosan todellisista hy\u00f6kk\u00e4yksist\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"aiheeseen-liittyvaa\">Aiheeseen liittyv\u00e4\u00e4<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/turvalliset-sessiot-nodejs\/\">Turvalliset sessiot Node.js:ss\u00e4: 10 vaihetta<\/a><\/li>\n<li><a href=\"\/hmac-node-js\/\">HMAC Node.js:ss\u00e4: 10 vaihetta, 30 min<\/a><\/li>\n<li><a href=\"\/salasanaturvallisuus\/\">Salasanaturvallisuus: vahvat salasanat ja niiden suojaus<\/a><\/li>\n<li><a href=\"\/digitaaliset-allekirjoitukset\/\">Digitaaliset allekirjoitukset: tiivisteet ja ep\u00e4symmetriset avaimet<\/a><\/li>\n<li><a href=\"\/https-ja-tls\/\">HTTPS ja TLS: miten salattu yhteys suojaa sinua<\/a><\/li>\n<li><a href=\"\/security-hub\/\">Verkkoturvallisuus: opas digitaalisen el\u00e4m\u00e4n suojaamiseen<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">L\u00e4hteet ja lis\u00e4lukemista: <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7519\" target=\"_blank\" rel=\"noopener nofollow\">RFC 7519 (JWT-standardi)<\/a>, <a href=\"https:\/\/jwt.io\/\" target=\"_blank\" rel=\"noopener nofollow\">jwt.io<\/a>, <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener nofollow\">OWASP Top Ten<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>JWT eli JSON Web Token on yleisin tapa todentaa k\u00e4ytt\u00e4ji\u00e4 moderneissa Node.js-rajapinnoissa. Token kulkee jokaisen pyynn\u00f6n mukana, palvelin tarkistaa allekirjoituksen eik\u00e4 istuntoja tarvitse tallentaa tietokantaan. T\u00e4m\u00e4 opas rakentaa t\u00e4yden kirjautumisj\u00e4rjestelm\u00e4n\u2026<\/p>\n","protected":false},"author":4,"featured_media":90,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-89","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-10","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/89","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/comments?post=89"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/89\/revisions"}],"predecessor-version":[{"id":91,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/89\/revisions\/91"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media\/90"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media?parent=89"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/categories?post=89"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/tags?post=89"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}