{"id":140,"date":"2026-06-20T20:52:48","date_gmt":"2026-06-20T20:52:48","guid":{"rendered":"https:\/\/shattered.io\/fi\/2026\/06\/20\/oauth2-passport-nodejs\/"},"modified":"2026-06-28T23:48:45","modified_gmt":"2026-06-28T23:48:45","slug":"oauth2-passport-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/fi\/oauth2-passport-nodejs\/","title":{"rendered":"OAuth 2.0 Node.js:ss\u00e4 Passport.js:ll\u00e4: 12 vaihetta, 30 min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth 2.0 on t\u00e4n\u00e4 p\u00e4iv\u00e4n\u00e4 k\u00e4yt\u00e4nn\u00f6ss\u00e4 pakollinen osa jokaista Node.js-sovellusta, jossa k\u00e4ytt\u00e4j\u00e4t kirjautuvat sis\u00e4\u00e4n Google-, GitHub- tai Microsoft-tileill\u00e4. Passport.js on de facto -standardi OAuth 2.0 -todennuksen toteuttamiseen Node.js:ss\u00e4: kirjasto ker\u00e4\u00e4 yli 1,4 miljoonaa viikoittaista npm-latausta ja on tuotantok\u00e4yt\u00f6ss\u00e4 sadoissa tuhansissa sovelluksissa maailmanlaajuisesti. T\u00e4ss\u00e4 oppaassa k\u00e4yt l\u00e4pi 12 vaihetta toimivaan OAuth 2.0 -toteutukseen Express.js-sovelluksessa, PKCE-turvaprotokolla mukaan lukien, Redis-pohjainen istuntohallinta sek\u00e4 tuotantovalmis uloskirjautuminen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"mita-oauth-2-0-tarkoittaa-ja-miksi-se-on-valttamaton\">Mit\u00e4 OAuth 2.0 tarkoittaa ja miksi se on v\u00e4ltt\u00e4m\u00e4t\u00f6n<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 (RFC 6749) on valtuutuskehys, joka mahdollistaa kolmansien osapuolten sovellusten rajoitetun p\u00e4\u00e4syn k\u00e4ytt\u00e4j\u00e4tilille ilman salasanan jakamista. Protokolla erottaa todennuksen (kuka olet) ja valtuutuksen (mit\u00e4 saat tehd\u00e4) toisistaan selke\u00e4sti. Kun k\u00e4ytt\u00e4j\u00e4 kirjautuu Node.js-sovellukseesi Google-tilill\u00e4\u00e4n, Google toimii valtuutuspalvelimena ja sovelluksesi resurssien k\u00e4ytt\u00e4j\u00e4n\u00e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Perinteiseen salasanapohjaiseen todennukseen verrattuna OAuth 2.0 tarjoaa kolme keskeist\u00e4 etua. Ensiksi sovelluksesi ei koskaan k\u00e4sittele k\u00e4ytt\u00e4j\u00e4n salasanaa, joten tietomurtojen vakavuus pienenee merkitt\u00e4v\u00e4sti. Toiseksi k\u00e4ytt\u00e4j\u00e4t kirjautuvat sis\u00e4\u00e4n yhdell\u00e4 klikkauksella olemassa olevilla tileill\u00e4 ilman uuden salasanan luomista. Kolmanneksi k\u00e4ytt\u00f6oikeuksia rajataan tarkasti: sovellus pyyt\u00e4\u00e4 vain tarvitsemansa scope-arvot, kuten <code>profile<\/code> ja <code>email<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OpenID Connect (OIDC) rakentuu OAuth 2.0:n p\u00e4\u00e4lle ja lis\u00e4\u00e4 todennuskerroksen ID-tokenin muodossa. Google, Microsoft ja GitHub tukevat kaikki OIDC-laajennusta, joten sovelluksesi saa sek\u00e4 valtuutuksen (access token) ett\u00e4 identiteettitiedon (ID token) yhdell\u00e4 virralla. Passport.js abstrahoi t\u00e4m\u00e4n monimutkaisuuden yksinkertaiseksi rajapinnaksi.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"esivaatimukset-ja-versiot\">Esivaatimukset ja versiot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ennen aloittamista varmista, ett\u00e4 kehitysymp\u00e4rist\u00f6si t\u00e4ytt\u00e4\u00e4 seuraavat vaatimukset. Versioyhteensopivuus on kriittinen, sill\u00e4 Passport.js:ss\u00e4 tapahtui merkitt\u00e4vi\u00e4 muutoksia 0.6.0-versiosta alkaen:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Vaatimus<\/th><th>Minimi versio<\/th><th>Suositeltu<\/th><th>Huomio<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>18.x LTS<\/td><td>22.x LTS<\/td><td>Pitk\u00e4aikaistuki vaaditaan<\/td><\/tr><tr><td>npm<\/td><td>9.x<\/td><td>10.x<\/td><td>Package lock v3<\/td><\/tr><tr><td>passport<\/td><td>0.6.0<\/td><td>0.7.0<\/td><td>req.logout() vaatii callbackin 0.6+ versioissa<\/td><\/tr><tr><td>passport-google-oauth20<\/td><td>2.0.0<\/td><td>2.0.0<\/td><td>Vakain Google-strategia<\/td><\/tr><tr><td>passport-github2<\/td><td>0.3.0<\/td><td>0.3.0<\/td><td>GitHub OAuth2 -strategia<\/td><\/tr><tr><td>express-session<\/td><td>1.17.3<\/td><td>1.18.1<\/td><td>Tietoturvap\u00e4ivitykset sis\u00e4ltyv\u00e4t<\/td><\/tr><tr><td>connect-redis<\/td><td>7.x<\/td><td>8.x<\/td><td>Redis-istuntotallennus<\/td><\/tr><tr><td>Redis<\/td><td>6.x<\/td><td>7.x<\/td><td>Docker tai paikallinen asennus<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Tarvitset my\u00f6s Google Cloud Console -tilin OAuth2-tunnusten luomiseen. GitHub Developer Settings riitt\u00e4\u00e4 GitHub-strategiaa varten. T\u00e4ss\u00e4 oppaassa Google-strategia toimii p\u00e4\u00e4esimerkkin\u00e4, mutta samat periaatteet p\u00e4tev\u00e4t kaikkiin Passport.js-strategioihin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-1-google-cloud-projekti-ja-oauth2-tunnukset\">Vaihe 1: Google Cloud -projekti ja OAuth2-tunnukset<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Google-pohjaisen OAuth2-todennuksen k\u00e4ytt\u00f6\u00f6notto alkaa Google Cloud Consolesta. Avaa <strong>console.cloud.google.com<\/strong>, luo uusi projekti tai valitse olemassa oleva. Siirry kohtaan &#8220;APIs &amp; Services&#8221; ja valitse &#8220;Credentials&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Klikkaa &#8220;Create Credentials&#8221; ja valitse &#8220;OAuth client ID&#8221;. Valitse sovelluksen tyypiksi &#8220;Web application&#8221; ja anna sille kuvaava nimi. Lis\u00e4\u00e4 valtuutetut uudelleenohjaus-URI:t:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Kehitys: <code>http:\/\/localhost:3000\/auth\/google\/callback<\/code><\/li><li>Tuotanto: <code>https:\/\/sovelluksesi.fi\/auth\/google\/callback<\/code><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Google luo <strong>Client ID:n<\/strong> ja <strong>Client Secretin<\/strong>. Tallenna n\u00e4m\u00e4 heti, sill\u00e4 Client Secret n\u00e4ytet\u00e4\u00e4n vain kerran. N\u00e4m\u00e4 tunnukset eiv\u00e4t saa koskaan p\u00e4\u00e4ty\u00e4 versionhallintaan tai lokitiedostoihin. Jos tunnukset vuotavat, peruuta ne v\u00e4litt\u00f6m\u00e4sti Cloud Consolessa ja luo uudet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ota k\u00e4ytt\u00f6\u00f6n tarvittavat Google API:t: &#8220;People API&#8221; on nykysuosituksen mukainen rajapinta k\u00e4ytt\u00e4j\u00e4profiilitietojen hakemiseen. Aktivoi se &#8220;APIs &amp; Services&#8221; \/ &#8220;Library&#8221; -valikosta ennen testaamista, muuten callback ep\u00e4onnistuu 403-virheell\u00e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-2-projektin-alustaminen-ja-pakettien-asennus\">Vaihe 2: Projektin alustaminen ja pakettien asennus<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Luo uusi Node.js-projekti ja asenna tarvittavat riippuvuudet. Projekti k\u00e4ytt\u00e4\u00e4 Express.js-kehyst\u00e4 yhdistettyn\u00e4 Passport.js:\u00e4\u00e4n ja Redis-pohjaiseen istuntotallennukseen tuotantovalmiuden varmistamiseksi:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir passport-oauth2-demo\ncd passport-oauth2-demo\nnpm init -y\n\n# Ydinriippuvuudet\nnpm install express passport passport-google-oauth20 express-session\n\n# GitHub-strategia (valinnainen)\nnpm install passport-github2\n\n# Tuotantoistunto Redis-tallennuksella\nnpm install connect-redis redis\n\n# Ymp\u00e4rist\u00f6muuttujat\nnpm install dotenv\n\n# Kehitysty\u00f6kalut\nnpm install --save-dev nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luodaan projektirakenne, joka noudattaa MVC-periaatetta ja pit\u00e4\u00e4 autentikaatiologiikan erill\u00e4\u00e4n reiteist\u00e4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>passport-oauth2-demo\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 config\/\n\u2502   \u2502   \u251c\u2500\u2500 passport.js      # Passport-strategiat\n\u2502   \u2502   \u2514\u2500\u2500 session.js       # Istuntokonfiguraatio\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u2514\u2500\u2500 auth.js          # Todennuksen v\u00e4liohjelmat\n\u2502   \u251c\u2500\u2500 routes\/\n\u2502   \u2502   \u251c\u2500\u2500 auth.js          # OAuth-reitit\n\u2502   \u2502   \u2514\u2500\u2500 protected.js     # Suojatut reitit\n\u2502   \u2514\u2500\u2500 app.js               # Express-sovellus\n\u251c\u2500\u2500 .env                     # Ymp\u00e4rist\u00f6muuttujat (ei git:iin)\n\u251c\u2500\u2500 .env.example             # Esimerkki muuttujista\n\u251c\u2500\u2500 .gitignore\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lis\u00e4\u00e4 <code>package.json<\/code>-tiedostoon k\u00e4ynnistyskomennot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"scripts\": {\n    \"start\": \"node src\/app.js\",\n    \"dev\": \"nodemon src\/app.js\"\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-3-ymparistomuuttujat-ja-salaisuuksien-hallinta\">Vaihe 3: Ymp\u00e4rist\u00f6muuttujat ja salaisuuksien hallinta<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Luo <code>.env<\/code>-tiedosto projektin juureen. T\u00e4rke\u00e4\u00e4: lis\u00e4\u00e4 <code>.env<\/code> v\u00e4litt\u00f6m\u00e4sti <code>.gitignore<\/code>-tiedostoon ennen ensimm\u00e4ist\u00e4k\u00e4\u00e4n git-commitia. Google Client Secret -vuodot ovat yksi yleisimmist\u00e4 OAuth2-tietoturvavuodoista, ja GitHubin automaattiset skannerit havaitsevat ne minuuteissa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env - EI KOSKAAN VERSIONHALLINTAAN\nNODE_ENV=development\nPORT=3000\n\n# Google OAuth2\nGOOGLE_CLIENT_ID=123456789-abcdefghijklmnop.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=GOCSPX-salainenAvain123\n\n# GitHub OAuth2 (valinnainen)\nGITHUB_CLIENT_ID=Ov23liYourClientId\nGITHUB_CLIENT_SECRET=ghsec_yourSecretHere\n\n# Istunnon salainen avain (v\u00e4hint\u00e4\u00e4n 32 merkki\u00e4, satunnainen)\nSESSION_SECRET=satunnainen-salainen-avain-pitaa-olla-vahintaan-64-merkki\n\n# Redis-yhteys\nREDIS_URL=redis:\/\/localhost:6379\n\n# Sovelluksen URL (callback-URL:ien generointiin)\nAPP_URL=http:\/\/localhost:3000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Session-salaisuuden tulee olla kryptografisesti satunnainen merkkijono. Generoi se Node.js:n <code>crypto<\/code>-moduulilla:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n# Esimerkki tulosteesta:\n# 8f3a2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luo my\u00f6s <code>.env.example<\/code>-tiedosto versionhallintaan. Se n\u00e4ytt\u00e4\u00e4 vaaditut muuttujat ilman todellisia arvoja, jolloin tiimin muiden j\u00e4senten on helppo konfiguroida kehitysymp\u00e4rist\u00f6ns\u00e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-4-redis-istuntopalvelimen-konfiguraatio\">Vaihe 4: Redis-istuntopalvelimen konfiguraatio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tuotantosovelluksessa istuntoja ei tule tallentaa muistiin, koska palvelimen uudelleenk\u00e4ynnistys poistaa kaikki aktiiviset istunnot. Redis on suosituin Node.js-istuntotallennus: alle 1 millisekunnin latenssi, automaattinen TTL-vanheneminen ja klusterointituki.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/config\/session.js\nconst session = require('express-session');\nconst RedisStore = require('connect-redis').default;\nconst { createClient } = require('redis');\n\nasync function createSessionConfig() {\n  const redisClient = createClient({\n    url: process.env.REDIS_URL || 'redis:\/\/localhost:6379',\n  });\n\n  redisClient.on('error', (err) => {\n    console.error('Redis-virhe:', err);\n  });\n\n  await redisClient.connect();\n  console.log('Redis-yhteys muodostettu');\n\n  const store = new RedisStore({\n    client: redisClient,\n    prefix: 'sess:',\n    ttl: 86400,  \/\/ 24 tuntia sekunneissa\n  });\n\n  return session({\n    store,\n    secret: process.env.SESSION_SECRET,\n    resave: false,\n    saveUninitialized: false,\n    name: '__session',  \/\/ Ei oletusarvoista 'connect.sid' nime\u00e4\n    cookie: {\n      secure: process.env.NODE_ENV === 'production',\n      httpOnly: true,\n      sameSite: 'lax',\n      maxAge: 24 * 60 * 60 * 1000,  \/\/ 24 tuntia millisekunteina\n    },\n  });\n}\n\nmodule.exports = { createSessionConfig };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kolme kriittist\u00e4 cookie-asetusta tuotannossa: <strong>secure: true<\/strong> pakottaa HTTPS-yhteyden istuntoev\u00e4steelle, <strong>httpOnly: true<\/strong> est\u00e4\u00e4 JavaScript-p\u00e4\u00e4syn ev\u00e4steeseen (XSS-suoja) ja <strong>sameSite: &#8216;lax&#8217;<\/strong> suojaa CSRF-hy\u00f6kk\u00e4yksilt\u00e4 salliein silti normaalit sivustolinkit. Aseta <strong>resave: false<\/strong> ja <strong>saveUninitialized: false<\/strong> est\u00e4\u00e4ksesi turhia Redis-kirjoituksia.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-5-passport-js-konfiguraatio-google-strategialla\">Vaihe 5: Passport.js-konfiguraatio Google-strategialla<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Passport.js:n konfiguraatio koostuu kolmesta osasta: strategian m\u00e4\u00e4rittelyst\u00e4, k\u00e4ytt\u00e4j\u00e4n serialisoinnista istuntoon ja deserialioinnista takaisin pyynt\u00f6olioon. T\u00e4m\u00e4 kolmijako on Passport.js:n keskeinen arkkitehtuurinen p\u00e4\u00e4t\u00f6s, joka mahdollistaa istuntopohjaisen OAuth2-todennuksen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/config\/passport.js\nconst passport = require('passport');\nconst GoogleStrategy = require('passport-google-oauth20').Strategy;\n\n\/\/ K\u00e4ytt\u00e4j\u00e4tietokanta (kehityksess\u00e4 Map, tuotannossa SQL\/NoSQL-tietokanta)\nconst users = new Map();\n\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: process.env.GOOGLE_CLIENT_ID,\n      clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n      callbackURL: `${process.env.APP_URL}\/auth\/google\/callback`,\n      scope: ['openid', 'profile', 'email'],\n      \/\/ PKCE aktivoitu (RFC 7636 - suositeltava 2025-2026)\n      pkce: true,\n      state: true,\n    },\n    async (accessToken, refreshToken, profile, done) => {\n      try {\n        \/\/ Etsi tai luo k\u00e4ytt\u00e4j\u00e4 tietokannasta\n        let user = users.get(profile.id);\n\n        if (!user) {\n          user = {\n            id: profile.id,\n            displayName: profile.displayName,\n            email: profile.emails?.[0]?.value,\n            photo: profile.photos?.[0]?.value,\n            provider: 'google',\n            createdAt: new Date().toISOString(),\n          };\n          users.set(profile.id, user);\n          console.log('Uusi k\u00e4ytt\u00e4j\u00e4 rekister\u00f6ity:', user.email);\n        } else {\n          \/\/ P\u00e4ivit\u00e4 kirjautumisaika\n          user.lastLogin = new Date().toISOString();\n        }\n\n        return done(null, user);\n      } catch (error) {\n        return done(error, null);\n      }\n    }\n  )\n);\n\n\/\/ Serialisointi: vain k\u00e4ytt\u00e4j\u00e4-ID tallennetaan istuntoon\npassport.serializeUser((user, done) => {\n  done(null, user.id);\n});\n\n\/\/ Deserialiointi: ID muunnetaan k\u00e4ytt\u00e4j\u00e4olioksi joka pyynn\u00f6ll\u00e4\npassport.deserializeUser(async (id, done) => {\n  try {\n    const user = users.get(id);\n    done(null, user || false);\n  } catch (error) {\n    done(error, null);\n  }\n});\n\nmodule.exports = passport;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kaksi kriittist\u00e4 asetusta Google-strategiassa: <strong>pkce: true<\/strong> aktivoi PKCE-protokollan (Proof Key for Code Exchange, RFC 7636), joka est\u00e4\u00e4 authorization code -vaihdon kaappaamishy\u00f6kk\u00e4ykset. <strong>state: true<\/strong> generoi automaattisesti CSRF-suojaavan state-parametrin, joka tarkistetaan callback-vaiheessa. Ilman n\u00e4it\u00e4 asetuksia OAuth2-implementaatio on altis tunnetuille hy\u00f6kk\u00e4yksille.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-6-oauth2-reittien-konfiguraatio\">Vaihe 6: OAuth2-reittien konfiguraatio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2-virta vaatii kaksi reitti\u00e4: k\u00e4ynnistysreitin, joka ohjaa k\u00e4ytt\u00e4j\u00e4n Googlen kirjautumissivulle, ja callback-reitin, johon Google palauttaa k\u00e4ytt\u00e4j\u00e4n todennuksen j\u00e4lkeen. N\u00e4iden reittien oikea toteutus on kriittinen tietoturvan kannalta.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/auth.js\nconst express = require('express');\nconst passport = require('..\/config\/passport');\nconst router = express.Router();\n\n\/\/ Vaihe 1: K\u00e4ynnist\u00e4 OAuth2-virta - ohjaa Googlen kirjautumissivulle\nrouter.get(\n  '\/google',\n  passport.authenticate('google', {\n    scope: ['openid', 'profile', 'email'],\n    \/\/ Pakota tilin valinta joka kerta\n    prompt: 'select_account',\n  })\n);\n\n\/\/ Vaihe 2: Google palauttaa k\u00e4ytt\u00e4j\u00e4n t\u00e4nne todennuksen j\u00e4lkeen\nrouter.get(\n  '\/google\/callback',\n  passport.authenticate('google', {\n    failureRedirect: '\/auth\/virhe',\n    failureMessage: true,\n  }),\n  (req, res) => {\n    \/\/ Onnistunut todennus - ohjaa tallennettuun osoitteeseen tai dashboardiin\n    const redirectTo = req.session.returnTo || '\/dashboard';\n    delete req.session.returnTo;\n    res.redirect(redirectTo);\n  }\n);\n\n\/\/ Uloskirjautuminen - kolme pakollista vaihetta\nrouter.post('\/logout', (req, res, next) => {\n  req.logout((err) => {\n    if (err) return next(err);\n    \/\/ Tuhoa koko istunto Redis-tallennuksesta\n    req.session.destroy((destroyErr) => {\n      if (destroyErr) {\n        console.error('Istunnon tuhoaminen ep\u00e4onnistui:', destroyErr);\n      }\n      \/\/ Poista istuntoevaste selaimesta\n      res.clearCookie('__session');\n      res.redirect('\/');\n    });\n  });\n});\n\n\/\/ Virhesivu todennusep\u00e4onnistumisille\nrouter.get('\/virhe', (req, res) => {\n  res.status(401).json({\n    virhe: 'Todennus ep\u00e4onnistui',\n    viesti: req.session.messages?.[0] || 'Tuntematon virhe',\n  });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Uloskirjautumisessa on kolme pakollista vaihetta: <code>req.logout()<\/code> poistaa k\u00e4ytt\u00e4j\u00e4n Passport-istunnosta, <code>req.session.destroy()<\/code> tuhoaa koko istunnon Redis-tallennuksesta ja <code>res.clearCookie()<\/code> poistaa istuntoev\u00e4steen selaimesta. Kaikkien kolmen vaiheen suorittaminen on pakollista t\u00e4ydelliselle ja turvalliselle uloskirjautumiselle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-7-todennuksen-valiohjelmisto-suojatuille-reiteille\">Vaihe 7: Todennuksen v\u00e4liohjelmisto suojatuille reiteille<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Luo uudelleenk\u00e4ytett\u00e4v\u00e4 v\u00e4liohjelmisto, joka tarkistaa k\u00e4ytt\u00e4j\u00e4n todennuksen ennen suojatun sis\u00e4ll\u00f6n n\u00e4ytt\u00e4mist\u00e4. Hyvin toteutettu v\u00e4liohjelmisto tallentaa alkuper\u00e4isen URL:n ja ohjaa k\u00e4ytt\u00e4j\u00e4n sinne kirjautumisen j\u00e4lkeen, parantaen merkitt\u00e4v\u00e4sti k\u00e4ytt\u00f6kokemusta.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/auth.js\n\n\/\/ Vaatii kirjautumisen - ohjaa OAuth2-kirjautumiseen jos ei kirjautunut\nfunction requireAuth(req, res, next) {\n  if (req.isAuthenticated()) {\n    return next();\n  }\n  \/\/ Tallenna alkuper\u00e4inen URL paluua varten\n  req.session.returnTo = req.originalUrl;\n  res.redirect('\/auth\/google');\n}\n\n\/\/ Vaatii tietyn roolin tai oikeuden\nfunction requireRole(role) {\n  return (req, res, next) => {\n    if (!req.isAuthenticated()) {\n      req.session.returnTo = req.originalUrl;\n      return res.redirect('\/auth\/google');\n    }\n    if (req.user?.role !== role) {\n      return res.status(403).json({ virhe: 'Riitt\u00e4m\u00e4tt\u00f6m\u00e4t k\u00e4ytt\u00f6oikeudet' });\n    }\n    next();\n  };\n}\n\n\/\/ REST API -pyyn\u00f6ille: palauta 401 JSON, ei uudelleenohjausta\nfunction requireAuthAPI(req, res, next) {\n  if (req.isAuthenticated()) {\n    return next();\n  }\n  res.status(401).json({\n    virhe: 'Todennus vaaditaan',\n    kirjautuminenUrl: '\/auth\/google',\n  });\n}\n\nmodule.exports = { requireAuth, requireRole, requireAuthAPI };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-8-paasovelluksen-kokoaminen\">Vaihe 8: P\u00e4\u00e4sovelluksen kokoaminen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kokoa kaikki osat yhteen <code>app.js<\/code>-tiedostossa. V\u00e4liohjelmoston j\u00e4rjestys on kriittinen: istunnon t\u00e4ytyy olla alustettuna ennen Passport.js:\u00e4\u00e4, ja Passport.js ennen reittej\u00e4. V\u00e4\u00e4r\u00e4ss\u00e4 j\u00e4rjestyksess\u00e4 sovellus kaatuu tai istunnot eiv\u00e4t toimi.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/app.js\nrequire('dotenv').config();\nconst express = require('express');\nconst passport = require('.\/config\/passport');\nconst { createSessionConfig } = require('.\/config\/session');\nconst authRoutes = require('.\/routes\/auth');\nconst { requireAuth, requireAuthAPI } = require('.\/middleware\/auth');\n\nasync function startApp() {\n  const app = express();\n\n  \/\/ Perusv\u00e4liohjelmat\n  app.use(express.json());\n  app.use(express.urlencoded({ extended: true }));\n\n  \/\/ Luottamukselliset proxy-otsikot (Nginx \/ load balancerin takana)\n  if (process.env.NODE_ENV === 'production') {\n    app.set('trust proxy', 1);\n  }\n\n  \/\/ Istunto - OLTAVA ennen Passport.js:\u00e4\u00e4\n  const sessionMiddleware = await createSessionConfig();\n  app.use(sessionMiddleware);\n\n  \/\/ Passport.js alustus - OLTAVA istunnon j\u00e4lkeen\n  app.use(passport.initialize());\n  app.use(passport.session());\n\n  \/\/ Reitit\n  app.use('\/auth', authRoutes);\n\n  \/\/ Julkinen etusivu\n  app.get('\/', (req, res) => {\n    if (req.isAuthenticated()) {\n      return res.json({\n        viesti: `Tervetuloa, ${req.user.displayName}!`,\n        kayttaja: {\n          nimi: req.user.displayName,\n          sahkoposti: req.user.email,\n        },\n      });\n    }\n    res.json({\n      viesti: 'Kirjaudu sis\u00e4\u00e4n Google-tilill\u00e4',\n      kirjautuminenUrl: '\/auth\/google',\n    });\n  });\n\n  \/\/ Suojattu dashboard - vaatii kirjautumisen\n  app.get('\/dashboard', requireAuth, (req, res) => {\n    res.json({\n      viesti: 'Tervetuloa suojatulle sivulle',\n      kayttaja: req.user,\n    });\n  });\n\n  \/\/ Suojattu REST API -p\u00e4\u00e4tepiste\n  app.get('\/api\/profiili', requireAuthAPI, (req, res) => {\n    res.json({ kayttaja: req.user });\n  });\n\n  \/\/ Virhek\u00e4sittely\n  app.use((err, req, res, next) => {\n    console.error('Sovellusvirhe:', err.message);\n    res.status(500).json({ virhe: 'Sis\u00e4inen palvelinvirhe' });\n  });\n\n  const PORT = process.env.PORT || 3000;\n  app.listen(PORT, () => {\n    console.log(`Palvelin k\u00e4ynniss\u00e4: http:\/\/localhost:${PORT}`);\n    console.log(`Kirjaudu sis\u00e4\u00e4n: http:\/\/localhost:${PORT}\/auth\/google`);\n  });\n}\n\nstartApp().catch(console.error);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-9-testaus-ja-odotettu-tulos\">Vaihe 9: Testaus ja odotettu tulos<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4ynnist\u00e4 Redis paikallisesti Docker-kontissa ja testaa koko autentikaatiovirta j\u00e4rjestelm\u00e4llisesti ennen tuotantoon siirtymist\u00e4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># K\u00e4ynnist\u00e4 Redis Docker-kontissa\ndocker run -d -p 6379:6379 --name redis-dev redis:7-alpine\n\n# Vahvista Redis-yhteys\nredis-cli ping\n# PONG\n\n# K\u00e4ynnist\u00e4 sovellus kehitystilassa\nnpm run dev\n\n# Odotettu terminaalituloste:\n# Redis-yhteys muodostettu\n# Palvelin k\u00e4ynniss\u00e4: http:\/\/localhost:3000\n# Kirjaudu sis\u00e4\u00e4n: http:\/\/localhost:3000\/auth\/google\n\n# Testaa suojattu API ilman kirjautumista\ncurl http:\/\/localhost:3000\/api\/profiili\n# {\"virhe\":\"Todennus vaaditaan\",\"kirjautuminenUrl\":\"\/auth\/google\"}\n\n# Testaa julkinen etusivu\ncurl http:\/\/localhost:3000\/\n# {\"viesti\":\"Kirjaudu sis\u00e4\u00e4n Google-tilill\u00e4\",\"kirjautuminenUrl\":\"\/auth\/google\"}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Avaa selain ja siirry osoitteeseen <code>http:\/\/localhost:3000\/auth\/google<\/code>. Sovellus ohjaa sinut Googlen kirjautumissivulle. Kirjautumisen j\u00e4lkeen Google palauttaa sinut osoitteeseen <code>\/dashboard<\/code> ja saat vastauksen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"viesti\": \"Tervetuloa suojatulle sivulle\",\n  \"kayttaja\": {\n    \"id\": \"112233445566778899\",\n    \"displayName\": \"Matti Meik\u00e4l\u00e4inen\",\n    \"email\": \"matti@example.com\",\n    \"photo\": \"https:\/\/lh3.googleusercontent.com\/a\/ACg8ocJ...\",\n    \"provider\": \"google\",\n    \"createdAt\": \"2026-06-20T10:30:00.000Z\",\n    \"lastLogin\": \"2026-06-20T11:45:22.000Z\"\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-10-github-strategian-lisaaminen-rinnakkaiseksi-tarjoajaksi\">Vaihe 10: GitHub-strategian lis\u00e4\u00e4minen rinnakkaiseksi tarjoajaksi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Useimmat sovellukset tukevat useita OAuth2-tarjoajia. GitHub-strategian lis\u00e4\u00e4minen Passport.js:\u00e4\u00e4n on suoraviivaista. Ensin luo OAuth-sovellus GitHubissa: Settings, Developer settings, OAuth Apps, New OAuth App. Callback URL:ksi aseta <code>http:\/\/localhost:3000\/auth\/github\/callback<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lis\u00e4\u00e4 src\/config\/passport.js -tiedostoon Google-strategian j\u00e4lkeen\nconst GitHubStrategy = require('passport-github2').Strategy;\n\npassport.use(\n  new GitHubStrategy(\n    {\n      clientID: process.env.GITHUB_CLIENT_ID,\n      clientSecret: process.env.GITHUB_CLIENT_SECRET,\n      callbackURL: `${process.env.APP_URL}\/auth\/github\/callback`,\n      scope: ['user:email'],\n    },\n    async (accessToken, refreshToken, profile, done) => {\n      try {\n        \/\/ GitHub-s\u00e4hk\u00f6posti voi olla yksityinen - hae emails-taulukosta\n        const email = profile.emails?.find(e => e.primary)?.value\n                   || profile.emails?.[0]?.value;\n\n        const userId = `github_${profile.id}`;\n        let user = users.get(userId);\n\n        if (!user) {\n          user = {\n            id: userId,\n            displayName: profile.displayName || profile.username,\n            email,\n            photo: profile.photos?.[0]?.value,\n            provider: 'github',\n            githubUsername: profile.username,\n            createdAt: new Date().toISOString(),\n          };\n          users.set(userId, user);\n        }\n\n        return done(null, user);\n      } catch (error) {\n        return done(error, null);\n      }\n    }\n  )\n);\n\n\/\/ Lis\u00e4\u00e4 src\/routes\/auth.js -tiedostoon GitHub-reitit:\nrouter.get('\/github', passport.authenticate('github'));\n\nrouter.get(\n  '\/github\/callback',\n  passport.authenticate('github', { failureRedirect: '\/auth\/virhe' }),\n  (req, res) => {\n    const redirectTo = req.session.returnTo || '\/dashboard';\n    delete req.session.returnTo;\n    res.redirect(redirectTo);\n  }\n);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-11-access-token-paivitys-pitkaaikaisissa-istunnoissa\">Vaihe 11: Access Token -p\u00e4ivitys pitk\u00e4aikaisissa istunnoissa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Google-access tokenien elinik\u00e4 on 1 tunti. Pitk\u00e4aikaisissa sovelluksissa, joissa tarvitaan jatkuvaa p\u00e4\u00e4sy\u00e4 Google API:hin, t\u00e4ytyy toteuttaa automaattinen token-p\u00e4ivitys refresh tokenin avulla. T\u00e4m\u00e4 vaatii <code>accessType: 'offline'<\/code> -asetuksen Google-strategiassa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ P\u00e4ivitetty Google-strategia refresh token -tuella\npassport.use(\n  new GoogleStrategy(\n    {\n      clientID: process.env.GOOGLE_CLIENT_ID,\n      clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n      callbackURL: `${process.env.APP_URL}\/auth\/google\/callback`,\n      pkce: true,\n      state: true,\n      accessType: 'offline',   \/\/ Pyyd\u00e4 refresh token\n      prompt: 'consent',       \/\/ Pakottaa refresh tokenin saamisen uudelleenkin\n    },\n    async (accessToken, refreshToken, params, profile, done) => {\n      const expiresAt = Date.now() + (params.expires_in || 3600) * 1000;\n\n      const user = {\n        id: profile.id,\n        displayName: profile.displayName,\n        email: profile.emails?.[0]?.value,\n        provider: 'google',\n        \/\/ Tokenit tallennetaan tietokantaan (ei istuntoon)\n        tokens: {\n          accessToken,\n          refreshToken,    \/\/ Tallenna salattuna tietokantaan\n          expiresAt,\n        },\n        createdAt: new Date().toISOString(),\n      };\n\n      users.set(profile.id, user);\n      return done(null, user);\n    }\n  )\n);\n\n\/\/ Apufunktio access tokenin p\u00e4ivitt\u00e4miseen\nasync function refreshAccessToken(user) {\n  const { createClient } = require('@googleapis\/oauth2');\n  const oauth2Client = createClient({\n    clientId: process.env.GOOGLE_CLIENT_ID,\n    clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  });\n\n  oauth2Client.setCredentials({\n    refresh_token: user.tokens.refreshToken,\n  });\n\n  const { credentials } = await oauth2Client.refreshAccessToken();\n  user.tokens.accessToken = credentials.access_token;\n  user.tokens.expiresAt = credentials.expiry_date;\n\n  \/\/ Tallenna p\u00e4ivitetyt tokenit tietokantaan\n  users.set(user.id, user);\n  return user;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Refresh tokenit ovat eritt\u00e4in arkaluonteisia: niiden avulla voidaan hankkia uusia access tokeneja ilman k\u00e4ytt\u00e4j\u00e4n l\u00e4sn\u00e4oloa. Tallenna refresh tokenit aina salattuna tietokantaan, esimerkiksi AES-256-GCM-salauksella. \u00c4l\u00e4 koskaan tallenna niit\u00e4 istuntoon, lokitiedostoihin tai ymp\u00e4rist\u00f6muuttujiin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-12-tuotantovalmius-ja-tietoturvan-vahvistaminen\">Vaihe 12: Tuotantovalmius ja tietoturvan vahvistaminen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tuotantosovelluksessa on useita lis\u00e4asetuksia, jotka parantavat tietoturvaa ja suorituskyky\u00e4 merkitt\u00e4v\u00e4sti. Asenna Helmet.js HTTP-tietoturvaotsikoita varten ja lis\u00e4\u00e4 nopeusrajoitin autentikaatioreiteille brute force -hy\u00f6kk\u00e4ysten torjumiseksi.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install helmet express-rate-limit<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Lis\u00e4\u00e4 src\/app.js -tiedostoon ennen reittej\u00e4\nconst helmet = require('helmet');\nconst rateLimit = require('express-rate-limit');\n\n\/\/ HTTP-tietoturva-otsikot Helmetin avulla\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\"],\n      styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n      imgSrc: [\"'self'\", \"https:\/\/lh3.googleusercontent.com\", \"data:\"],\n    },\n  },\n  hsts: {\n    maxAge: 31536000,   \/\/ 1 vuosi\n    includeSubDomains: true,\n    preload: true,\n  },\n}));\n\n\/\/ Nopeusrajoitin autentikaatioreiteille\nconst authLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,   \/\/ 15 minuutin ikkuna\n  max: 20,                     \/\/ max 20 yrityst\u00e4 per IP\n  message: {\n    virhe: 'Liian monta kirjautumisyrityst\u00e4. Yrit\u00e4 15 minuutin kuluttua.',\n  },\n  standardHeaders: true,\n  legacyHeaders: false,\n});\n\napp.use('\/auth', authLimiter);<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"yleisimmat-sudenkuopat-7-virhetta-joita-kehittajat-tekevat\">Yleisimm\u00e4t sudenkuopat: 7 virhett\u00e4 joita kehitt\u00e4j\u00e4t tekev\u00e4t<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2-toteutuksissa toistuvat samat virheet projekteista toiseen. T\u00e4ss\u00e4 seitsem\u00e4n kriittisint\u00e4 sudenkuoppaa ja niiden ratkaisut:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Virhe<\/th><th>Tietoturvavaikutus<\/th><th>Ratkaisu<\/th><\/tr><\/thead><tbody><tr><td>State-parametri puuttuu<\/td><td>CSRF-hy\u00f6kk\u00e4ys mahdollinen<\/td><td>Aseta <code>state: true<\/code> tai k\u00e4sittele manuaalisesti<\/td><\/tr><tr><td>Client Secret versionhallinnassa<\/td><td>Koko OAuth-integraatio vaarantuu<\/td><td>.env + .gitignore, tarkista git-historia<\/td><\/tr><tr><td>Istunto muistissa (MemoryStore)<\/td><td>Istunnot h\u00e4vi\u00e4v\u00e4t palvelimen uudelleenk\u00e4ynnistyksess\u00e4<\/td><td>Redis tai muu pysyv\u00e4 tallennusratkaisu<\/td><\/tr><tr><td>Callback URL ei t\u00e4sm\u00e4\u00e4<\/td><td>Google hylk\u00e4\u00e4 pyynn\u00f6n<\/td><td>Tarkista Cloud Consolesta, URL t\u00e4sm\u00e4lleen sama<\/td><\/tr><tr><td>req.logout() ilman callbackia<\/td><td>Sovellus kaatuu Passport 0.6+ versioissa<\/td><td>K\u00e4yt\u00e4 <code>req.logout((err) =&gt; {...})<\/code><\/td><\/tr><tr><td>trust proxy puuttuu<\/td><td>Secure-ev\u00e4steet eiv\u00e4t toimi proxyjen takana<\/td><td><code>app.set('trust proxy', 1)<\/code><\/td><\/tr><tr><td>Avoin uudelleenohjaus returnTo:ssa<\/td><td>Phishing-hy\u00f6kk\u00e4ys kirjautumisen j\u00e4lkeen<\/td><td>Hyv\u00e4ksy vain suhteelliset URL:t<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudenkuoppa 1: Istunnon kiinnittymishy\u00f6kk\u00e4ys (Session Fixation).<\/strong> Jos istunto-ID ei uusiudu kirjautumisen yhteydess\u00e4, hy\u00f6kk\u00e4\u00e4j\u00e4 voi ennalta asettaa uhrin istunto-ID:n ja kaapata session heti kirjautumisen j\u00e4lkeen. Passport.js regeneroi istunnon automaattisesti, mutta varmista, ettet kutsu <code>req.session.save()<\/code> ennen kirjautumista tavalla joka est\u00e4\u00e4 t\u00e4m\u00e4n regeneroinnin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudenkuoppa 2: PKCE puuttuu mobiili- tai SPA-sovelluksesta.<\/strong> Ilman PKCE:t\u00e4 (RFC 7636) authorization code voidaan kaapata man-in-the-middle-hy\u00f6kk\u00e4yksell\u00e4 erityisesti mobiilisovelluksissa. IETF:n uusin BCP suosittelee PKCE:n k\u00e4ytt\u00f6\u00e4 my\u00f6s palvelinpuolen sovelluksissa. Passport.js 0.7.0 tukee PKCE:t\u00e4 <code>pkce: true<\/code> -asetuksella.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudenkuoppa 3: Refresh token puuttuu.<\/strong> Ilman <code>accessType: 'offline'<\/code> ja <code>prompt: 'consent'<\/code> -asetuksia Google ei palauta refresh tokenia. Access token vanhenee tunnin kuluttua eik\u00e4 sovellus pysty uusimaan sit\u00e4 automaattisesti, mik\u00e4 pakottaa k\u00e4ytt\u00e4j\u00e4n kirjautumaan uudelleen tunnin v\u00e4lein.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudenkuoppa 4: Koko k\u00e4ytt\u00e4j\u00e4olio serialisoidaan istuntoon.<\/strong> Tallenna istuntoon vain k\u00e4ytt\u00e4j\u00e4-ID. Deserialiointi lataa tuoreen k\u00e4ytt\u00e4j\u00e4tiedon tietokannasta joka pyynn\u00f6ll\u00e4, jolloin k\u00e4ytt\u00f6oikeuksien muutokset astuvat voimaan v\u00e4litt\u00f6m\u00e4sti ilman uudelleenkirjautumista.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudenkuoppa 5: Turvaton uudelleenohjaus.<\/strong> Jos <code>returnTo<\/code>-parametri hyv\u00e4ksyy mink\u00e4 tahansa URL:n, hy\u00f6kk\u00e4\u00e4j\u00e4 voi ohjata k\u00e4ytt\u00e4j\u00e4n phishing-sivulle kirjautumisen j\u00e4lkeen. Tarkista aina, ett\u00e4 uudelleenohjaus-URL on suhteellinen tai kuuluu sallittuihin domaineihin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vianmaaritys-8-yleisinta-ongelmaa\">Vianm\u00e4\u00e4ritys: 8 yleisint\u00e4 ongelmaa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 1: &#8220;redirect_uri_mismatch&#8221; -virhe Googlelta.<\/strong> Google hylk\u00e4\u00e4 pyynn\u00f6n, koska callback URL ei t\u00e4sm\u00e4\u00e4 Cloud Consolessa rekister\u00f6ityyn URL:iin t\u00e4sm\u00e4lleen. Protokolla (http vs https), portti ja polku t\u00e4ytyy olla identtiset. Kehityksess\u00e4 k\u00e4yt\u00e4 <code>http:\/\/localhost:3000<\/code>, ei <code>http:\/\/127.0.0.1:3000<\/code>, vaikka molemmat viittaavat samaan osoitteeseen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 2: Istunto h\u00e4vi\u00e4\u00e4 joka pyynn\u00f6ll\u00e4.<\/strong> Jos <code>req.user<\/code> on aina <code>undefined<\/code> vaikka kirjautuminen onnistui, ongelma on yleens\u00e4 puuttuva <code>passport.session()<\/code>-v\u00e4liohjelmisto tai virheellinen serialisointi. Tarkista, ett\u00e4 <code>passport.serializeUser<\/code> ja <code>passport.deserializeUser<\/code> on m\u00e4\u00e4ritelty ja ett\u00e4 istunto on alustettu ennen Passport.js:\u00e4\u00e4.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 3: &#8220;Cannot read properties of undefined&#8221; deserialioinnissa.<\/strong> T\u00e4m\u00e4 tapahtuu kun k\u00e4ytt\u00e4j\u00e4\u00e4 ei l\u00f6ydy tietokannasta serialisoidulla ID:ll\u00e4. Varmista, ett\u00e4 kehityksess\u00e4 k\u00e4ytett\u00e4v\u00e4 in-memory Map ei tyhjenee palvelimen uudelleenk\u00e4ynnistysten v\u00e4lill\u00e4. Tuotannossa k\u00e4yt\u00e4 aina pysyv\u00e4\u00e4 tietokantaa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 4: Redis-yhteys katkeaa.<\/strong> Virhe <code>Error: Redis connection refused<\/code> tarkoittaa, ettei Redis ole k\u00e4ynniss\u00e4 tai yhteysosoite on v\u00e4\u00e4r\u00e4. Tarkista <code>REDIS_URL<\/code>-ymp\u00e4rist\u00f6muuttuja ja Redis-palvelimen tila: <code>redis-cli ping<\/code> palauttaa <code>PONG<\/code> jos yhteys toimii.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 5: Ev\u00e4steet eiv\u00e4t toimi tuotannossa.<\/strong> Jos <code>secure: true<\/code> on asetettu mutta sovellus toimii HTTP:n takana (esimerkiksi Nginx-proxyn takana), ev\u00e4steet eiv\u00e4t siirry. Aseta <code>app.set('trust proxy', 1)<\/code> Express-sovellukseen, jotta se luottaa Nginx:n <code>X-Forwarded-Proto: https<\/code> -otsikkoon.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 6: &#8220;req.logout() is not a function&#8221; tai kaatuu ilman callbackia.<\/strong> Passport.js 0.6:sta alkaen <code>req.logout()<\/code> vaatii pakollisen callback-funktion. Vanha muoto <code>req.logout()<\/code> ei toimi uusissa versioissa. Muuta kaikki kutsut muotoon <code>req.logout((err) => { ... })<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 7: K\u00e4ytt\u00e4j\u00e4 ohjataan kirjautumissivulle vaikka on kirjautunut.<\/strong> Jos <code>req.isAuthenticated()<\/code> palauttaa <code>false<\/code> vaikka istunto on olemassa, ongelma on usein eri istuntoavain kehityksess\u00e4 ja tuotannossa tai muuttunut <code>SESSION_SECRET<\/code>. Kaikki olemassa olevat istunnot mit\u00e4t\u00f6ityv\u00e4t kun salaisuus vaihtuu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ongelma 8: &#8220;invalid_grant&#8221; Google API -vastauksessa.<\/strong> Authorization code on kertak\u00e4ytt\u00f6inen ja vanhenee noin 10 minuutissa. T\u00e4m\u00e4 virhe tarkoittaa, ett\u00e4 koodia on yritetty k\u00e4ytt\u00e4\u00e4 uudelleen tai se on vanhentunut. Tarkista, ettei callback-reitti aktivoidu kahdesti (esimerkiksi favicon.ico-pyynt\u00f6 ei saa laukaista OAuth-callbackia).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"oauth-2-0-vs-jwt-vs-perinteinen-istuntokirjautuminen\">OAuth 2.0 vs JWT vs perinteinen istuntokirjautuminen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kehitt\u00e4j\u00e4t sekoittavat usein OAuth 2.0:n, JWT:n ja istuntopohjaisen todennuksen. N\u00e4m\u00e4 eiv\u00e4t ole toisensa poissulkevia: OAuth 2.0 on valtuutuskehys, JWT on tokenmuoto ja istunnot ovat tallennusmekanismi. Niit\u00e4 voidaan ja usein pit\u00e4\u00e4kin k\u00e4ytt\u00e4\u00e4 yhdess\u00e4.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Ominaisuus<\/th><th>OAuth2 + Istunnot (Passport.js)<\/th><th>OAuth2 + JWT<\/th><th>Salasana + Istunnot<\/th><\/tr><\/thead><tbody><tr><td>Palvelintilan tarve<\/td><td>Kyll\u00e4 (Redis)<\/td><td>Ei (stateless)<\/td><td>Kyll\u00e4 (Redis)<\/td><\/tr><tr><td>Skaalautuvuus<\/td><td>Redis-klusterilla hyv\u00e4<\/td><td>Erinomainen<\/td><td>Redis-klusterilla hyv\u00e4<\/td><\/tr><tr><td>V\u00e4lit\u00f6n uloskirjautuminen<\/td><td>Kyll\u00e4<\/td><td>Vaatii denylist-toteutuksen<\/td><td>Kyll\u00e4<\/td><\/tr><tr><td>Mobiilisovellukset<\/td><td>Rajoitettu<\/td><td>Erinomainen<\/td><td>Mahdollinen<\/td><\/tr><tr><td>K\u00e4ytt\u00e4j\u00e4n salasana sovelluksessa<\/td><td>Ei<\/td><td>Ei (OAuth2:lla)<\/td><td>Kyll\u00e4 (bcrypt\/Argon2)<\/td><\/tr><tr><td>Toteutuksen monimutkaisuus<\/td><td>Kohtalainen<\/td><td>Korkea<\/td><td>Matala<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Verkkosovelluksille, joissa on perinteinen selain-palvelin-arkkitehtuuri, OAuth 2.0 yhdistettyn\u00e4 Redis-istuntoihin on yksinkertaisin ja turvallisin ratkaisu. REST API -palveluille, joita kuluttavat mobiilisovellukset tai muut mikropalvelut, JWT-tokenit OAuth 2.0:n kanssa ovat parempi valinta, koska ne mahdollistavat stateless-arkkitehtuurin ilman istuntotallennuksen monimutkaisuutta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"edistynyt-postgresql-tietokantaintegraatio-ja-tilin-yhdistaminen\">Edistynyt: PostgreSQL-tietokantaintegraatio ja tilin yhdist\u00e4minen<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"tietokantaskeema-useille-oauth2-tarjoajille\">Tietokantaskeema useille OAuth2-tarjoajille<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tuotantosovelluksessa k\u00e4ytt\u00e4j\u00e4tiedot tallennetaan relaatiotietokantaan. Erota k\u00e4ytt\u00e4j\u00e4taulu ja OAuth-tilitaulu toisistaan, jolloin sama k\u00e4ytt\u00e4j\u00e4 voi kirjautua useilla eri tarjoajilla samaan tiliin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- PostgreSQL-skeema OAuth2-k\u00e4ytt\u00e4jille\nCREATE TABLE users (\n  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  email VARCHAR(255) UNIQUE NOT NULL,\n  display_name VARCHAR(255),\n  photo_url TEXT,\n  created_at TIMESTAMP DEFAULT NOW(),\n  last_login TIMESTAMP\n);\n\nCREATE TABLE oauth_accounts (\n  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n  user_id UUID REFERENCES users(id) ON DELETE CASCADE,\n  provider VARCHAR(50) NOT NULL,           -- 'google', 'github', 'microsoft'\n  provider_id VARCHAR(255) NOT NULL,\n  access_token TEXT,\n  refresh_token TEXT,                       -- Tallenna AES-256-GCM-salattuna\n  token_expires_at TIMESTAMP,\n  created_at TIMESTAMP DEFAULT NOW(),\n  UNIQUE(provider, provider_id)\n);\n\n-- Indeksi nopeaan hakuun kirjautumisen yhteydess\u00e4\nCREATE INDEX idx_oauth_provider ON oauth_accounts(provider, provider_id);<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"automaattinen-tilin-yhdistaminen-sahkopostiosoitteen-perusteella\">Automaattinen tilin yhdist\u00e4minen s\u00e4hk\u00f6postiosoitteen perusteella<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4ytt\u00e4j\u00e4ll\u00e4 voi olla sama s\u00e4hk\u00f6postiosoite sek\u00e4 Google- ett\u00e4 GitHub-tilill\u00e4. Ilman tilin yhdist\u00e4mist\u00e4 sama henkil\u00f6 saa kaksi erillist\u00e4 k\u00e4ytt\u00e4j\u00e4tili\u00e4, mik\u00e4 johtaa ep\u00e4johdonmukaiseen kokemukseen. Toteuta tilin yhdist\u00e4minen OAuth2-callback-funktiossa:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ K\u00e4yt\u00e4 t\u00e4t\u00e4 funktiota GoogleStrategy- ja GitHubStrategy-callbackeissa\nasync function findOrCreateUser(db, profile, provider) {\n  const email = profile.emails?.[0]?.value;\n\n  \/\/ 1. Etsi ensin OAuth-tilin perusteella (nopein polku)\n  const oauthResult = await db.query(\n    `SELECT u.* FROM users u\n     JOIN oauth_accounts oa ON u.id = oa.user_id\n     WHERE oa.provider = $1 AND oa.provider_id = $2`,\n    [provider, profile.id]\n  );\n\n  if (oauthResult.rows.length > 0) {\n    \/\/ P\u00e4ivit\u00e4 kirjautumisaika\n    await db.query('UPDATE users SET last_login = NOW() WHERE id = $1', [oauthResult.rows[0].id]);\n    return oauthResult.rows[0];\n  }\n\n  \/\/ 2. Yhdist\u00e4 olemassa olevaan tiliin s\u00e4hk\u00f6postin perusteella\n  let userId;\n  if (email) {\n    const emailResult = await db.query('SELECT id FROM users WHERE email = $1', [email]);\n    userId = emailResult.rows[0]?.id;\n  }\n\n  \/\/ 3. Luo uusi k\u00e4ytt\u00e4j\u00e4 jos ei l\u00f6ydy\n  if (!userId) {\n    const newUser = await db.query(\n      'INSERT INTO users (email, display_name, photo_url) VALUES ($1, $2, $3) RETURNING id',\n      [email, profile.displayName, profile.photos?.[0]?.value]\n    );\n    userId = newUser.rows[0].id;\n  }\n\n  \/\/ 4. Tallenna OAuth-tili\n  await db.query(\n    'INSERT INTO oauth_accounts (user_id, provider, provider_id) VALUES ($1, $2, $3)',\n    [userId, provider, profile.id]\n  );\n\n  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);\n  return user.rows[0];\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"tietoturvatarkistuslista-ennen-tuotantoonsiirtoa\">Tietoturvatarkistuslista ennen tuotantoonsiirtoa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4y l\u00e4pi t\u00e4m\u00e4 tarkistuslista ennen sovelluksen julkaisua. Jokainen kohta on kriittinen OAuth2-toteutuksen tietoturvalle:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>PKCE aktivoitu<\/strong>: <code>pkce: true<\/code> Passport.js-strategiassa<\/li><li><strong>State-parametri k\u00e4yt\u00f6ss\u00e4<\/strong>: <code>state: true<\/code> CSRF-suojaksi<\/li><li><strong>HTTPS pakollinen<\/strong>: <code>secure: true<\/code> ev\u00e4steille, HTTPS callback-URL<\/li><li><strong>Client Secret ymp\u00e4rist\u00f6muuttujassa<\/strong>: ei koodissa, ei git-historiassa<\/li><li><strong>Redis-istuntotallennus<\/strong>: ei MemoryStore tuotannossa<\/li><li><strong>Istunnon TTL asetettu<\/strong>: vanhenemisaika sek\u00e4 Redisiin (ttl) ett\u00e4 ev\u00e4steeseen (maxAge)<\/li><li><strong>Ev\u00e4steen nimi muutettu<\/strong>: ei oletusarvoista <code>connect.sid<\/code><\/li><li><strong>trust proxy asetettu<\/strong>: load balancerin tai Nginx:n takana<\/li><li><strong>Nopeusrajoitin autentikointireiteill\u00e4<\/strong>: suojaa brute force -hy\u00f6kk\u00e4yksilt\u00e4<\/li><li><strong>Helmet.js k\u00e4yt\u00f6ss\u00e4<\/strong>: HTTP-tietoturva-otsikot asetettu<\/li><li><strong>Uudelleenohjauksen validointi<\/strong>: vain suhteelliset URL:t hyv\u00e4ksyt\u00e4\u00e4n returnTo:ssa<\/li><li><strong>Refresh tokenit salattu tietokantaan<\/strong>: ei selkotekstin\u00e4, ei istuntoon<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"usein-kysytyt-kysymykset\">Usein kysytyt kysymykset<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Onko Passport.js edelleen paras valinta Node.js OAuth2 -toteutukseen vuonna 2026?<\/strong><br>Passport.js on edelleen suosituin ratkaisu yli 1,4 miljoonan viikoittaisen npm-latauksen perusteella. Vaihtoehdot kuten <code>openid-client<\/code> (OpenID Connect -spesifinen kirjasto) tai Auth.js (NextAuth.js:n seuraaja) ovat hyvi\u00e4 vaihtoehtoja modernimpiin projekteihin. Passport.js sopii parhaiten Express-pohjaisiin sovelluksiin, joissa tarvitaan useita todennusstrategioita rinnakkain.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mik\u00e4 on ero OAuth 2.0:n ja OpenID Connectin v\u00e4lill\u00e4?<\/strong><br>OAuth 2.0 on valtuutuskehys, joka antaa sovellukselle p\u00e4\u00e4syn resursseihin k\u00e4ytt\u00e4j\u00e4n puolesta, mutta se ei itsess\u00e4\u00e4n todenna k\u00e4ytt\u00e4j\u00e4\u00e4. OpenID Connect rakentuu OAuth 2.0:n p\u00e4\u00e4lle ja lis\u00e4\u00e4 todennuksen ID-tokenin muodossa. K\u00e4yt\u00e4nn\u00f6ss\u00e4 Google, GitHub ja Microsoft tukevat OIDC:t\u00e4, joten saat sek\u00e4 valtuutuksen ett\u00e4 identiteetin samassa virrassa pyyt\u00e4m\u00e4ll\u00e4 <code>openid<\/code>-scopen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pit\u00e4\u00e4k\u00f6 PKCE:t\u00e4 k\u00e4ytt\u00e4\u00e4 palvelinpuolen Node.js-sovelluksissa?<\/strong><br>PKCE on alun perin suunniteltu julkisille asiakkaille, mutta IETF:n uusin BCP (OAuth 2.0 Security Best Current Practice) suosittelee PKCE:n k\u00e4ytt\u00f6\u00e4 my\u00f6s palvelinpuolen sovelluksissa. Passport.js 0.7.0 tukee PKCE:t\u00e4 <code>pkce: true<\/code> -asetuksella. Suositus on ottaa se k\u00e4ytt\u00f6\u00f6n kaikissa uusissa projekteissa.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kuinka tallentaa access ja refresh tokenit turvallisesti?<\/strong><br>Tallenna refresh tokenit aina salattuna tietokantaan, esimerkiksi AES-256-GCM-salauksella, ennen tallentamista. Access tokeneita ei yleens\u00e4 tarvitse tallentaa pysyv\u00e4sti, koska ne haetaan uudelleen refresh tokenin avulla tarvittaessa. \u00c4l\u00e4 koskaan tallenna tokeneita istuntoon, lokitiedostoihin tai ymp\u00e4rist\u00f6muuttujiin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Miten toteuttaa uloskirjautuminen kaikista laitteista samanaikaisesti?<\/strong><br>Poista kaikki k\u00e4ytt\u00e4j\u00e4n istunnot Redisist\u00e4 k\u00e4ytt\u00e4m\u00e4ll\u00e4 SCAN-komentoa. Tehokkaampi tapa on k\u00e4ytt\u00e4\u00e4 k\u00e4ytt\u00e4j\u00e4kohtaisia Redis-avaimia: tallenna istunto-ID:t k\u00e4ytt\u00e4j\u00e4-ID:n alle (<code>user:sessions:{userId}<\/code>) ja poista ne kaikki kerralla uloskirjautumisen yhteydess\u00e4.<\/p>\n<!-- \/wp:parameter>\n\n\n<p class=\"wp-block-paragraph\"><strong>Kuinka monta OAuth2-tarjoajaa Passport.js tukee?<\/strong><br>Passport.js:lle on saatavilla yli 500 virallista ja yhteis\u00f6n yll\u00e4pit\u00e4m\u00e4\u00e4 strategiaa, mukaan lukien kaikki suuret OAuth2-tarjoajat: Google, GitHub, Microsoft, Facebook, Twitter\/X, LinkedIn, Slack, Discord, Spotify, Apple, Twitch ja monet muut. L\u00f6yd\u00e4t kaikki strategiat osoitteesta <a href=\"https:\/\/www.passportjs.org\/packages\/\" target=\"_blank\" rel=\"noopener\">passportjs.org\/packages<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Voinko k\u00e4ytt\u00e4\u00e4 Passport.js:\u00e4\u00e4 Fastify- tai Hono-kehyksen kanssa?<\/strong><br>Passport.js on suunniteltu Express.js:lle ja Connect-yhteensopivien kehysten kanssa. Fastify-k\u00e4ytt\u00e4jille on saatavilla <code>@fastify\/passport<\/code>-paketti. Hono:lle tai muille kevyemmille kehyksille <code>openid-client<\/code>-kirjasto on parempi valinta suoran OAuth2\/OIDC-integraation tarpeisiin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mik\u00e4 on turvallisin tapa testata OAuth2 paikallisesti ilman oikeita tunnuksia?<\/strong><br>K\u00e4yt\u00e4 paikallista OAuth2-palvelinta, kuten Docker-pohjaista Keycloak-instanssia tai <code>node-oidc-provider<\/code>-pakettia. N\u00e4m\u00e4 mahdollistavat t\u00e4ydellisen OAuth2-virran testaamisen ilman oikeita pilvipalvelutunnuksia kehitysvaiheessa. Keycloak on hyv\u00e4 valinta, koska se tukee sek\u00e4 OAuth2:ta ett\u00e4 OIDC:t\u00e4 t\u00e4ysin ja on helppo konfiguroida Dockerilla.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"aiheeseen-liittyvaa-luettavaa\">Aiheeseen liittyv\u00e4\u00e4 luettavaa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2-todennus kytkeytyy useisiin muihin Node.js-tietoturvan osa-alueisiin. N\u00e4m\u00e4 oppaat auttavat rakentamaan kattavan tietoturvastrategian:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/jwt-todennus-nodejs\/\">JWT-todennus Node.js:ss\u00e4: 12 vaihetta, 40 min [2026]<\/a><\/li><li><a href=\"\/turvalliset-sessiot-nodejs\/\">Turvalliset sessiot Node.js:ss\u00e4: 10 vaihetta [2026]<\/a><\/li><li><a href=\"\/nodejs-api-avainten-hallinta\/\">Node.js API-avainten hallinta: 12 vaihetta, 30 min [2026]<\/a><\/li><li><a href=\"\/webcrypto-api-nodejs\/\">Node.js WebCrypto API: 12 vaihetta, 35 min [2026]<\/a><\/li><li><a href=\"\/hmac-node-js\/\">HMAC Node.js:ss\u00e4: 10 vaihetta, 30 min [2026]<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Lis\u00e4\u00e4 tietoturva-aiheita: <a href=\"\/security\/\">Kyberturvallisuusoppaat: API-tietoturva, todennus ja salaus<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Viralliset standardit ja dokumentaatio:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" rel=\"noopener\" target=\"_blank\">RFC 6749: The OAuth 2.0 Authorization Framework (IETF)<\/a><\/li><li><a href=\"https:\/\/oauth.net\/2\/pkce\/\" rel=\"noopener\" target=\"_blank\">PKCE: Proof Key for Code Exchange (OAuth.net)<\/a><\/li><li><a href=\"https:\/\/openid.net\/connect\/\" rel=\"noopener\" target=\"_blank\">OpenID Connect Core 1.0 (OpenID Foundation)<\/a><\/li><li><a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/OAuth2_Cheat_Sheet.html\" rel=\"noopener\" target=\"_blank\">OWASP OAuth2 Security Cheat Sheet<\/a><\/li><li><a href=\"https:\/\/www.passportjs.org\/packages\/passport-oauth2\/\" rel=\"noopener\" target=\"_blank\">Passport.js OAuth2 -strategian dokumentaatio<\/a><\/li><li><a href=\"https:\/\/expressjs.com\/en\/resources\/middleware\/session.html\" rel=\"noopener\" target=\"_blank\">Express Session -v\u00e4liohjelmiston dokumentaatio<\/a><\/li><\/ul>\n\n\n\n\n\n","protected":false},"excerpt":{"rendered":"<p>OAuth 2.0 on t\u00e4n\u00e4 p\u00e4iv\u00e4n\u00e4 k\u00e4yt\u00e4nn\u00f6ss\u00e4 pakollinen osa jokaista Node.js-sovellusta, jossa k\u00e4ytt\u00e4j\u00e4t kirjautuvat sis\u00e4\u00e4n Google-, GitHub- tai Microsoft-tileill\u00e4. Passport.js on de facto -standardi OAuth 2.0 -todennuksen toteuttamiseen Node.js:ss\u00e4: kirjasto ker\u00e4\u00e4\u2026<\/p>\n","protected":false},"author":6,"featured_media":141,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-140","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\/140","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\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/comments?post=140"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/140\/revisions"}],"predecessor-version":[{"id":142,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/140\/revisions\/142"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media\/141"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media?parent=140"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/categories?post=140"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/tags?post=140"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}