{"id":119,"date":"2026-06-17T20:22:16","date_gmt":"2026-06-17T20:22:16","guid":{"rendered":"https:\/\/shattered.io\/at\/2026\/06\/17\/oauth2-nodejs\/"},"modified":"2026-06-25T06:56:08","modified_gmt":"2026-06-25T06:56:08","slug":"oauth2-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/at\/oauth2-nodejs\/","title":{"rendered":"OAuth 2.0 in Node.js: 12 Schritte, 45 Min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth 2.0 ist das meistgenutzte Autorisierungsprotokoll im Web. \u00dcber 90 Prozent aller modernen Web-APIs setzen auf OAuth 2.0 als Grundlage. Wer eine Node.js-Anwendung absichern will, die sich mit Google, GitHub oder einem eigenen Identity-Provider verbindet, kommt an OAuth 2.0 nicht vorbei. Dieses Tutorial zeigt den vollst\u00e4ndigen Implementierungsprozess: von der Projektinitialisierung \u00fcber den Authorization Code Flow mit PKCE bis hin zur Token-Revokation mit Redis. Alle Schritte sind auf Node.js mit Express ausgelegt und lassen sich in unter 45 Minuten umsetzen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"was-ist-oauth2\">Was ist OAuth 2.0 und warum ist es relevant?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 ist ein Autorisierungsframework, das in <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6749<\/a> spezifiziert ist. Es erm\u00f6glicht einer Anwendung, im Auftrag eines Benutzers eingeschr\u00e4nkten Zugriff auf eine andere Anwendung zu erhalten, ohne dass der Benutzer sein Passwort direkt weitergeben muss. Das klassische Beispiel: Eine Webanwendung darf die Google-Kalendereintr\u00e4ge eines Benutzers lesen, aber nicht sein Google-Passwort kennen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 ist kein Authentifizierungsprotokoll, sondern ein Autorisierungsprotokoll. Wer Authentifizierung (Identit\u00e4tspr\u00fcfung) braucht, nutzt OpenID Connect (OIDC), das auf OAuth 2.0 aufbaut. In der Praxis werden beide oft kombiniert. Dieses Tutorial behandelt den OAuth 2.0 Authorization Code Flow, erg\u00e4nzt durch PKCE (Proof Key for Code Exchange, spezifiziert in <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7636<\/a>), da dieser als sicherster und modernster Flow gilt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"die-vier-oauth-2-0-flows-im-vergleich\">Die vier OAuth 2.0 Flows im Vergleich<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Flow<\/th><th>Einsatzbereich<\/th><th>PKCE n\u00f6tig<\/th><th>Empfohlen 2026<\/th><\/tr><\/thead><tbody><tr><td>Authorization Code + PKCE<\/td><td>Webanwendungen, Native Apps, SPAs<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Client Credentials<\/td><td>Server-zu-Server (kein User)<\/td><td>Nein<\/td><td>Ja<\/td><\/tr><tr><td>Device Code<\/td><td>TV-Apps, CLI-Tools<\/td><td>Nein<\/td><td>Ja<\/td><\/tr><tr><td>Implicit Flow<\/td><td>Alte SPAs (veraltet)<\/td><td>Entf\u00e4llt<\/td><td>Nein, deprecated<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Der Implicit Flow gilt seit OAuth 2.1 als veraltet und soll nicht mehr eingesetzt werden. F\u00fcr alle modernen Anwendungen ist der Authorization Code Flow mit PKCE die richtige Wahl, auch f\u00fcr vertrauliche Clients (Server-seitige Apps). Der Resource Owner Password Credentials (ROPC) Flow ist ebenfalls veraltet und wird in OAuth 2.1 entfernt, da er das Passwort des Benutzers an den Client weitergibt, was dem Kernprinzip von OAuth widerspricht.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"voraussetzungen\">Voraussetzungen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Bevor die Implementierung beginnt, m\u00fcssen folgende Voraussetzungen erf\u00fcllt sein:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Node.js 20.x oder neuer<\/strong> (LTS-Version, pr\u00fcfen mit <code>node --version<\/code>)<\/li>\n<li><strong>npm 10.x<\/strong> oder neuer (kommt mit Node.js, pr\u00fcfen mit <code>npm --version<\/code>)<\/li>\n<li><strong>Ein Google Cloud-Konto<\/strong> f\u00fcr OAuth 2.0-Credentials (kostenlos)<\/li>\n<li><strong>Redis<\/strong> f\u00fcr Token-Revokation in Schritt 11 (optional, aber empfohlen)<\/li>\n<li>Grundkenntnisse in Express.js und HTTP-Grundlagen<\/li>\n<li>Ein Terminal mit Bash oder PowerShell<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Wer noch keine Erfahrung mit JWT hat, sollte zuerst das Tutorial zu <a href=\"\/at\/jwt-authentication-nodejs\/\">JWT Authentication in Node.js<\/a> lesen, da dieses Tutorial darauf aufbaut. Grundwissen zu Session-Management vermittelt das <a href=\"\/at\/nodejs-session-management\/\">Node.js Session Management Tutorial<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-1-projekt-setup\">Schritt 1: Projektstruktur anlegen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ein neues Node.js-Projekt wird initialisiert. Die Verzeichnisstruktur folgt einer klaren Trennung von Routen, Middleware und Konfiguration. Saubere Struktur erleichtert sp\u00e4tere Erweiterungen, zum Beispiel wenn weitere OAuth-Provider (GitHub, Microsoft) hinzukommen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir oauth2-nodejs-demo\ncd oauth2-nodejs-demo\nnpm init -y\n\n# Verzeichnisstruktur erstellen\nmkdir -p src\/routes src\/middleware src\/config src\/utils<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Projektstruktur sieht nach diesem Schritt so aus:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>oauth2-nodejs-demo\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 config\/\n\u2502   \u2502   \u251c\u2500\u2500 passport.js\n\u2502   \u2502   \u2514\u2500\u2500 redis.js\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u2514\u2500\u2500 auth.js\n\u2502   \u251c\u2500\u2500 routes\/\n\u2502   \u2502   \u251c\u2500\u2500 auth.js\n\u2502   \u2502   \u2514\u2500\u2500 protected.js\n\u2502   \u251c\u2500\u2500 utils\/\n\u2502   \u2502   \u2514\u2500\u2500 pkce.js\n\u2502   \u2514\u2500\u2500 app.js\n\u251c\u2500\u2500 .env\n\u251c\u2500\u2500 .env.example\n\u251c\u2500\u2500 .gitignore\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Wichtig:<\/strong> Die <code>.env<\/code>-Datei niemals in Git einchecken. Sie enth\u00e4lt Client-Secrets, die bei Verlust sofortige Sicherheitsrisiken erzeugen. Die <code>.gitignore<\/code>-Datei muss mindestens <code>.env<\/code> und <code>node_modules\/<\/code> enthalten. Der folgende Befehl erstellt eine <code>.gitignore<\/code>-Datei mit sinnvollen Standardwerten:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\nnode_modules\/\n.env\n*.log\ndist\/<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-2-google-oauth-app\">Schritt 2: Google OAuth 2.0-Anwendung einrichten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Google dient in diesem Tutorial als Identity Provider. Die Einrichtung dauert unter 5 Minuten und ist kostenlos:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Google Cloud Console aufrufen und ein neues Projekt erstellen (oder ein bestehendes w\u00e4hlen)<\/li>\n<li>Unter <strong>APIs &amp; Services &gt; OAuth-Zustimmungsseite<\/strong> den Anwendungstyp &#8220;extern&#8221; w\u00e4hlen und App-Name eintragen<\/li>\n<li>Unter <strong>APIs &amp; Services &gt; Zugangsdaten<\/strong> auf <strong>Zugangsdaten erstellen &gt; OAuth 2.0-Client-IDs<\/strong> klicken<\/li>\n<li>Anwendungstyp: <strong>Webanwendung<\/strong> ausw\u00e4hlen<\/li>\n<li>Autorisierte JavaScript-Quellen: <code>http:\/\/localhost:3000<\/code><\/li>\n<li>Autorisierte Weiterleitungs-URIs: <code>http:\/\/localhost:3000\/auth\/google\/callback<\/code><\/li>\n<li>Auf <strong>Erstellen<\/strong> klicken und Client-ID sowie Client-Secret notieren<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Die Redirect URI muss <strong>exakt<\/strong> \u00fcbereinstimmen. Google und andere Authorization Server verwenden String-Matching ohne Wildcard-Unterst\u00fctzung. Ein h\u00e4ufiger Fehler ist ein abschlie\u00dfender Slash (<code>\/callback\/<\/code> statt <code>\/callback<\/code>), der sofort zu einem <code>redirect_uri_mismatch<\/code>-Fehler f\u00fchrt. F\u00fcr Produktionsumgebungen wird eine zweite Redirect URI f\u00fcr die echte Domain registriert.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-3-dependencies\">Schritt 3: Dependencies installieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Das Projekt ben\u00f6tigt mehrere npm-Pakete. Hier eine \u00dcbersicht der verwendeten Packages mit Begr\u00fcndung:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Paket<\/th><th>Funktion<\/th><th>Kategorie<\/th><\/tr><\/thead><tbody><tr><td>express<\/td><td>HTTP-Framework f\u00fcr Routing und Middleware<\/td><td>Core<\/td><\/tr><tr><td>passport<\/td><td>Authentifizierungs-Middleware mit Strategy-Pattern<\/td><td>Auth<\/td><\/tr><tr><td>passport-google-oauth20<\/td><td>Google OAuth 2.0 Strategy f\u00fcr Passport<\/td><td>Auth<\/td><\/tr><tr><td>express-session<\/td><td>Session-Handling als Basis f\u00fcr Passport<\/td><td>Auth<\/td><\/tr><tr><td>cookie-parser<\/td><td>Cookie-Parsing f\u00fcr httpOnly-Token-Cookies<\/td><td>Auth<\/td><\/tr><tr><td>jsonwebtoken<\/td><td>JWT-Erstellung und -Validierung<\/td><td>Token<\/td><\/tr><tr><td>dotenv<\/td><td>Umgebungsvariablen aus .env-Datei laden<\/td><td>Config<\/td><\/tr><tr><td>crypto (built-in)<\/td><td>PKCE code_verifier generieren (kein npm n\u00f6tig)<\/td><td>Security<\/td><\/tr><tr><td>ioredis<\/td><td>Redis-Client f\u00fcr Token-Revokation und -Blocklist<\/td><td>Storage<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install express passport passport-google-oauth20 express-session cookie-parser jsonwebtoken dotenv ioredis\n\n# Development-Dependency f\u00fcr automatisches Neustarten\nnpm install --save-dev nodemon<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das Paket <code>crypto<\/code> ist in Node.js built-in und muss nicht separat installiert werden. Es wird f\u00fcr die PKCE-Implementierung in Schritt 8 ben\u00f6tigt. <code>nodemon<\/code> startet den Server bei Datei\u00e4nderungen automatisch neu, was die Entwicklung erheblich beschleunigt. Das <code>package.json<\/code>-Skript dazu:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ package.json (Ausschnitt)\n{\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=\"schritt-4-env-konfiguration\">Schritt 4: Umgebungsvariablen konfigurieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Alle sensiblen Werte kommen in die <code>.env<\/code>-Datei. Niemals Secrets direkt im Code hinterlegen, da sie sonst in der Versionskontrolle landen und bei einem versehentlichen Public-Repository-Push f\u00fcr alle sichtbar werden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nGOOGLE_CLIENT_ID=deine-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=dein-client-secret\nGOOGLE_CALLBACK_URL=http:\/\/localhost:3000\/auth\/google\/callback\n\nSESSION_SECRET=ein-starkes-zufaelliges-geheimnis-min-32-zeichen\nJWT_SECRET=ein-anderes-starkes-geheimnis-min-64-zeichen\nJWT_ACCESS_EXPIRES=15m\nJWT_REFRESH_EXPIRES=7d\n\nPORT=3000\nREDIS_URL=redis:\/\/localhost:6379\nNODE_ENV=development<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das <code>SESSION_SECRET<\/code> und <code>JWT_SECRET<\/code> m\u00fcssen kryptographisch zuf\u00e4llig sein. Ein geeignetes Secret l\u00e4sst sich so generieren:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Secret generieren (in der Terminal ausf\u00fchren)\nnode -e \"console.log(require('crypto').randomBytes(64).toString('hex'))\"\n# Ausgabe: a3f8c2d1e9b047... (128 Zeichen langer Hex-String)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die <code>.env.example<\/code>-Datei enth\u00e4lt dieselbe Struktur ohne echte Werte und kann sicher in Git eingecheckt werden, damit andere Entwickler wissen, welche Variablen gesetzt werden m\u00fcssen. Die Trennung von <code>SESSION_SECRET<\/code> und <code>JWT_SECRET<\/code> ist bewusst: Selbst wenn ein Secret kompromittiert wird, sind nicht beide Token-Typen betroffen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-5-express-app\">Schritt 5: Express-App und Middleware-Stack aufbauen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Hauptdatei <code>src\/app.js<\/code> verdrahtet alle Komponenten. Express-Session wird als Basis f\u00fcr Passport ben\u00f6tigt. Die Reihenfolge der Middleware ist entscheidend: Session muss vor Passport initialisiert werden, da Passport die Session f\u00fcr die User-Serialisierung nutzt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/app.js\nrequire('dotenv').config();\nconst express = require('express');\nconst session = require('express-session');\nconst cookieParser = require('cookie-parser');\nconst passport = require('passport');\n\nrequire('.\/config\/passport')(passport);\n\nconst authRoutes = require('.\/routes\/auth');\nconst protectedRoutes = require('.\/routes\/protected');\n\nconst app = express();\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\napp.use(cookieParser());\n\n\/\/ Reihenfolge ist kritisch: session() vor passport.initialize()\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    secure: process.env.NODE_ENV === 'production',\n    httpOnly: true,\n    maxAge: 15 * 60 * 1000 \/\/ 15 Minuten\n  }\n}));\n\napp.use(passport.initialize());\napp.use(passport.session());\n\napp.use('\/auth', authRoutes);\napp.use('\/api', protectedRoutes);\n\n\/\/ Globaler Fehlerhandler\napp.use((err, req, res, next) => {\n  console.error(err.message);\n  res.status(500).json({ error: 'Interner Serverfehler' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Server l\u00e4uft auf Port ${PORT}`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Session-Cookie-Konfiguration ist sicherheitsrelevant: <code>httpOnly: true<\/code> verhindert den Zugriff durch JavaScript und sch\u00fctzt vor XSS-Angriffen. <code>secure: true<\/code> in Produktion erzwingt HTTPS und verhindert, dass der Cookie \u00fcber unverschl\u00fcsselte Verbindungen \u00fcbertragen wird. Die kurze Session-Laufzeit von 15 Minuten deckt sich mit der empfohlenen Access-Token-Laufzeit. Mehr zu CSRF-Schutz in Express zeigt das <a href=\"\/at\/csrf-protection-nodejs\/\">CSRF Protection in Node.js Tutorial<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-6-passport-konfiguration\">Schritt 6: Passport mit Google Strategy konfigurieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.passportjs.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">Passport.js<\/a> ist eine Middleware f\u00fcr Node.js mit \u00fcber 500 verf\u00fcgbaren Strategien f\u00fcr verschiedene Anbieter und Protokolle. Die Google-Strategy \u00fcbernimmt den gesamten OAuth 2.0-Handshake mit Google, inklusive Code-Exchange und Profil-Abruf.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/config\/passport.js\nconst GoogleStrategy = require('passport-google-oauth20').Strategy;\n\nmodule.exports = (passport) => {\n  passport.use(new GoogleStrategy({\n    clientID: process.env.GOOGLE_CLIENT_ID,\n    clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n    callbackURL: process.env.GOOGLE_CALLBACK_URL,\n    scope: ['profile', 'email'],\n    state: true \/\/ Aktiviert automatischen CSRF-Schutz via state-Parameter\n  },\n  async (accessToken, refreshToken, profile, done) => {\n    try {\n      \/\/ Benutzerprofile aus Google extrahieren\n      const user = {\n        googleId: profile.id,\n        email: profile.emails[0].value,\n        name: profile.displayName,\n        picture: profile.photos[0]?.value\n      };\n\n      \/\/ Hier k\u00f6nnte eine Datenbankabfrage erfolgen:\n      \/\/ const dbUser = await User.findOrCreate({ googleId: profile.id }, user);\n      \/\/ return done(null, dbUser);\n\n      return done(null, user);\n    } catch (err) {\n      return done(err, null);\n    }\n  }));\n\n  \/\/ Benutzer in Session serialisieren (minimale Daten)\n  passport.serializeUser((user, done) => {\n    done(null, user);\n  });\n\n  passport.deserializeUser((user, done) => {\n    done(null, user);\n  });\n};<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Parameter <code>state: true<\/code> aktiviert den CSRF-Schutz im OAuth-Flow. Passport generiert dabei automatisch einen zuf\u00e4lligen State-Parameter, der in der Session gespeichert und im Callback verifiziert wird. Ohne State-Parameter ist der Flow anf\u00e4llig f\u00fcr CSRF-Angriffe. Die Scopes <code>profile<\/code> und <code>email<\/code> sind auf das Minimum reduziert: Eine App soll nur Zugriff auf das anfordern, was sie wirklich braucht.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-7-auth-routen\">Schritt 7: Auth-Routen implementieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Auth-Routen steuern den vollst\u00e4ndigen OAuth 2.0-Flow: Login-Initiierung, Callback-Verarbeitung, Token-Ausgabe und Logout. Nach erfolgreichem OAuth-Handshake werden eigene JWTs ausgestellt statt die Google-Tokens direkt weiterzuverwenden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/auth.js\nconst express = require('express');\nconst passport = require('passport');\nconst jwt = require('jsonwebtoken');\nconst router = express.Router();\n\n\/\/ Schritt 1 des OAuth-Flows: Benutzer zu Google weiterleiten\nrouter.get('\/google',\n  passport.authenticate('google', {\n    scope: ['profile', 'email'],\n    accessType: 'offline',  \/\/ Google Refresh Token anfordern\n    prompt: 'consent'       \/\/ Refresh Token bei jedem Login erzwingen\n  })\n);\n\n\/\/ Schritt 3 des OAuth-Flows: Google sendet Authorization Code zur\u00fcck\nrouter.get('\/google\/callback',\n  passport.authenticate('google', {\n    failureRedirect: '\/auth\/failure',\n    session: false \/\/ Keine Session f\u00fcr API-Token-Flow\n  }),\n  (req, res) => {\n    \/\/ Eigene JWTs ausstellen (Google-Token nicht direkt weiterverwenden)\n    const accessToken = jwt.sign(\n      {\n        userId: req.user.googleId,\n        email: req.user.email,\n        name: req.user.name\n      },\n      process.env.JWT_SECRET,\n      { expiresIn: process.env.JWT_ACCESS_EXPIRES || '15m' }\n    );\n\n    const refreshToken = jwt.sign(\n      { userId: req.user.googleId },\n      process.env.JWT_SECRET,\n      { expiresIn: process.env.JWT_REFRESH_EXPIRES || '7d' }\n    );\n\n    \/\/ Token als httpOnly-Cookie setzen (NICHT in localStorage!)\n    res.cookie('access_token', accessToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      sameSite: 'lax',\n      maxAge: 15 * 60 * 1000 \/\/ 15 Minuten in Millisekunden\n    });\n\n    res.cookie('refresh_token', refreshToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      sameSite: 'lax',\n      path: '\/auth\/refresh', \/\/ Nur f\u00fcr Refresh-Endpoint zug\u00e4nglich\n      maxAge: 7 * 24 * 60 * 60 * 1000 \/\/ 7 Tage\n    });\n\n    res.json({\n      message: 'Authentifizierung erfolgreich',\n      user: {\n        email: req.user.email,\n        name: req.user.name\n      }\n    });\n  }\n);\n\nrouter.get('\/failure', (req, res) => {\n  res.status(401).json({ error: 'Authentifizierung fehlgeschlagen' });\n});\n\nrouter.post('\/logout', (req, res) => {\n  res.clearCookie('access_token');\n  res.clearCookie('refresh_token', { path: '\/auth\/refresh' });\n  res.json({ message: 'Abgemeldet' });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tokens werden als <code>httpOnly<\/code>-Cookies gesetzt, nicht als <code>Authorization<\/code>-Header im Frontend gespeichert. Das verhindert, dass JavaScript-Code (und damit XSS-Angriffe) die Tokens stehlen kann. Das <code>sameSite: 'lax'<\/code>-Attribut bietet zus\u00e4tzlichen CSRF-Schutz. Der Refresh-Token-Cookie hat zus\u00e4tzlich einen eingeschr\u00e4nkten <code>path<\/code>, sodass er nur an den <code>\/auth\/refresh<\/code>-Endpoint gesendet wird und nicht bei jeder API-Anfrage mitgeschickt wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-8-pkce\">Schritt 8: PKCE implementieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE (Proof Key for Code Exchange, ausgesprochen &#8220;pixie&#8221;) ist seit <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7636<\/a> standardisiert und sch\u00fctzt den Authorization Code Flow vor Code-Injection-Angriffen. Das Prinzip: Der Client generiert vor dem Authorization Request einen zuf\u00e4lligen <code>code_verifier<\/code> (43-128 Zeichen) und berechnet daraus einen <code>code_challenge<\/code> (SHA-256-Hash). Die Challenge wird im Authorization Request mitgesendet. Beim Token-Request schickt der Client den originalen Verifier, den der Server gegen die gespeicherte Challenge verifiziert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr Custom OAuth-Flows ohne Passport (z. B. bei einem eigenen Authorization Server oder einem Provider der kein Passport-Paket hat) sieht die PKCE-Implementierung in Node.js so aus:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/utils\/pkce.js\nconst crypto = require('crypto');\n\nfunction generateCodeVerifier() {\n  \/\/ RFC 7636: 43-128 Zeichen, URL-safe Base64 (base64url, kein +, \/, =)\n  return crypto.randomBytes(32).toString('base64url');\n}\n\nfunction generateCodeChallenge(codeVerifier) {\n  \/\/ Nur S256-Methode verwenden, nie 'plain'\n  return crypto.createHash('sha256')\n    .update(codeVerifier)\n    .digest('base64url');\n}\n\nfunction buildAuthorizationUrl(params) {\n  const codeVerifier = generateCodeVerifier();\n  const codeChallenge = generateCodeChallenge(codeVerifier);\n  const state = crypto.randomBytes(16).toString('hex');\n\n  const url = new URL(params.authorizationEndpoint);\n  url.searchParams.set('response_type', 'code');\n  url.searchParams.set('client_id', params.clientId);\n  url.searchParams.set('redirect_uri', params.redirectUri);\n  url.searchParams.set('scope', params.scope);\n  url.searchParams.set('state', state);\n  url.searchParams.set('code_challenge', codeChallenge);\n  url.searchParams.set('code_challenge_method', 'S256');\n\n  \/\/ codeVerifier und state in Session speichern f\u00fcr sp\u00e4tere Verifizierung\n  return { url: url.toString(), codeVerifier, state };\n}\n\nasync function exchangeCodeForToken(params) {\n  const body = new URLSearchParams({\n    grant_type: 'authorization_code',\n    code: params.code,\n    redirect_uri: params.redirectUri,\n    client_id: params.clientId,\n    code_verifier: params.codeVerifier \/\/ Hier wird der Verifier mitgesendet\n  });\n\n  const response = await fetch(params.tokenEndpoint, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n    body: body.toString()\n  });\n\n  return response.json();\n}\n\nmodule.exports = { generateCodeVerifier, generateCodeChallenge, buildAuthorizationUrl, exchangeCodeForToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der <code>code_verifier<\/code> muss serverseitig zwischen Authorization Request und Token Request sicher gespeichert werden, zum Beispiel in der Session. Der SHA-256-Algorithmus (<code>S256<\/code>) ist die einzig zul\u00e4ssige Methode nach aktuellem Stand. Der veraltete <code>plain<\/code>-Modus bietet keinen echten Schutz und darf nicht verwendet werden, da er keinen Schutz vor einem passiven Angreifer bietet, der die PKCE-Parameter mitlesen kann.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-9-auth-middleware\">Schritt 9: Auth-Middleware und gesch\u00fctzte Routen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Eine Middleware pr\u00fcft bei jeder Anfrage das JWT-Access-Token aus dem Cookie und h\u00e4ngt die dekodierten User-Daten an das Request-Objekt. Gesch\u00fctzte Routen verwenden diese Middleware als Guard.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/auth.js\nconst jwt = require('jsonwebtoken');\n\nasync function authenticateToken(req, res, next) {\n  const token = req.cookies?.access_token;\n\n  if (!token) {\n    return res.status(401).json({ error: 'Kein Token vorhanden' });\n  }\n\n  try {\n    const decoded = jwt.verify(token, process.env.JWT_SECRET, {\n      \/\/ Issuer-Validierung verhindert Token-Confusion-Angriffe\n      \/\/ issuer: 'https:\/\/deine-app.com' \/\/ In Produktion aktivieren\n    });\n    req.user = decoded;\n    next();\n  } catch (err) {\n    if (err.name === 'TokenExpiredError') {\n      return res.status(401).json({\n        error: 'Token abgelaufen',\n        code: 'TOKEN_EXPIRED'\n      });\n    }\n    return res.status(403).json({ error: 'Ung\u00fcltiges Token' });\n  }\n}\n\nmodule.exports = { authenticateToken };<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/protected.js\nconst express = require('express');\nconst { authenticateToken } = require('..\/middleware\/auth');\nconst router = express.Router();\n\nrouter.get('\/profile', authenticateToken, (req, res) => {\n  res.json({\n    userId: req.user.userId,\n    email: req.user.email,\n    name: req.user.name\n  });\n});\n\nrouter.get('\/dashboard', authenticateToken, (req, res) => {\n  res.json({\n    message: `Willkommen, ${req.user.name}`,\n    tokenAusgestellt: new Date(req.user.iat * 1000).toISOString(),\n    tokenLaeuftAb: new Date(req.user.exp * 1000).toISOString()\n  });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Middleware unterscheidet explizit zwischen abgelaufenem Token (<code>TokenExpiredError<\/code>) und ung\u00fcltigem Token. Das erm\u00f6glicht dem Client, automatisch ein neues Token anzufordern, wenn der Fehlercode <code>TOKEN_EXPIRED<\/code> zur\u00fcckkommt, ohne den Benutzer zur erneuten Anmeldung zu zwingen. Ung\u00fcltige Tokens (falsche Signatur, manipulierter Payload) werden mit <code>403 Forbidden<\/code> abgelehnt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-10-refresh-token\">Schritt 10: Refresh-Token-Mechanismus implementieren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Access Tokens mit kurzer Laufzeit (5-15 Minuten) erh\u00f6hen die Sicherheit erheblich, da ein kompromittiertes Token nur kurze Zeit nutzbar ist. Refresh Tokens erm\u00f6glichen es, neue Access Tokens zu holen, ohne den Benutzer erneut zur Anmeldung zu zwingen. Die Laufzeit von Refresh Tokens liegt typischerweise bei 7-30 Tagen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/routes\/auth.js (Erweiterung mit Refresh-Endpoint)\nrouter.post('\/refresh', async (req, res) => {\n  const refreshToken = req.cookies?.refresh_token;\n\n  if (!refreshToken) {\n    return res.status(401).json({ error: 'Kein Refresh Token vorhanden' });\n  }\n\n  try {\n    const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);\n\n    \/\/ Neues Access Token mit voller Laufzeit ausstellen\n    const newAccessToken = jwt.sign(\n      { userId: decoded.userId },\n      process.env.JWT_SECRET,\n      { expiresIn: process.env.JWT_ACCESS_EXPIRES || '15m' }\n    );\n\n    res.cookie('access_token', newAccessToken, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      sameSite: 'lax',\n      maxAge: 15 * 60 * 1000\n    });\n\n    res.json({ message: 'Token erfolgreich erneuert' });\n\n  } catch (err) {\n    \/\/ Bei ung\u00fcltigem Refresh Token beide Cookies l\u00f6schen (vollst\u00e4ndiger Logout)\n    res.clearCookie('access_token');\n    res.clearCookie('refresh_token', { path: '\/auth\/refresh' });\n    return res.status(401).json({ error: 'Refresh Token ung\u00fcltig oder abgelaufen' });\n  }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ein kritischer Aspekt bei der Token-Erneuerung: Race Conditions. Wenn mehrere parallele API-Anfragen gleichzeitig ein abgelaufenes Token erkennen und alle gleichzeitig den Refresh-Endpoint aufrufen, kann das zu Fehlern f\u00fchren (der zweite Refresh-Request schl\u00e4gt fehl, wenn Refresh Token Rotation aktiv ist). In Produktionsumgebungen sollte ein &#8220;Single-Flight&#8221;-Mechanismus im Client implementiert werden: Nur die erste Anfrage f\u00fchrt den Refresh durch, alle anderen warten auf das Ergebnis.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-11-token-revokation\">Schritt 11: Token-Revokation mit Redis<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">JWTs sind zustandslos: Der Server kann ein ausgestelltes Token nicht direkt invalidieren, bevor es abl\u00e4uft. Das ist ein bekanntes Problem, zum Beispiel nach einem Passwort-Wechsel, nach einer Sicherheitsverletzung oder wenn ein Benutzer sich aktiv abmeldet. Die L\u00f6sung ist eine Token-Blocklist in Redis, die revozierte Tokens bis zu ihrer Ablaufzeit speichert. Der Speicherbedarf ist gering, da Eintr\u00e4ge automatisch nach Ablauf der Token-TTL entfernt werden.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/config\/redis.js\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL || 'redis:\/\/localhost:6379', {\n  lazyConnect: true,\n  maxRetriesPerRequest: 3\n});\n\nredis.on('error', (err) => {\n  console.error('Redis-Verbindungsfehler:', err.message);\n});\n\nredis.on('connect', () => {\n  console.log('Redis verbunden');\n});\n\nasync function revokeToken(token, expiresAt) {\n  const secondsUntilExpiry = Math.floor(expiresAt - Date.now() \/ 1000);\n  if (secondsUntilExpiry > 0) {\n    \/\/ Token bis zu seinem nat\u00fcrlichen Ablauf auf Blocklist setzen\n    await redis.set(`revoked:${token}`, '1', 'EX', secondsUntilExpiry);\n  }\n}\n\nasync function isTokenRevoked(token) {\n  const result = await redis.get(`revoked:${token}`);\n  return result !== null;\n}\n\nmodule.exports = { redis, revokeToken, isTokenRevoked };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Auth-Middleware wird um eine Redis-Pr\u00fcfung erweitert:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/auth.js (erweitert mit Redis-Blocklist)\nconst jwt = require('jsonwebtoken');\nconst { isTokenRevoked } = require('..\/config\/redis');\n\nasync function authenticateToken(req, res, next) {\n  const token = req.cookies?.access_token;\n\n  if (!token) {\n    return res.status(401).json({ error: 'Kein Token vorhanden' });\n  }\n\n  try {\n    \/\/ Zuerst Blocklist pr\u00fcfen (vor JWT-Verify f\u00fcr schnelles Fail)\n    const revoked = await isTokenRevoked(token);\n    if (revoked) {\n      return res.status(401).json({ error: 'Token revoziert' });\n    }\n\n    const decoded = jwt.verify(token, process.env.JWT_SECRET);\n    req.user = decoded;\n    next();\n  } catch (err) {\n    if (err.name === 'TokenExpiredError') {\n      return res.status(401).json({ error: 'Token abgelaufen', code: 'TOKEN_EXPIRED' });\n    }\n    return res.status(403).json({ error: 'Ung\u00fcltiges Token' });\n  }\n}\n\nmodule.exports = { authenticateToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die TTL des Redis-Eintrags entspricht der verbleibenden Token-Laufzeit. Redis l\u00f6scht den Eintrag automatisch, sobald das Token ohnehin abgelaufen w\u00e4re. Das h\u00e4lt die Redis-Datenbank sauber und der Speicherbedarf der Blocklist w\u00e4chst nicht unkontrolliert. F\u00fcr Redis-Betrieb in lokaler Entwicklung reicht ein Docker-Befehl: <code>docker run -p 6379:6379 redis:alpine<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"schritt-12-client-credentials\">Schritt 12: Client Credentials Flow f\u00fcr Server-zu-Server-APIs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn zwei Server miteinander kommunizieren (z. B. ein Microservice ruft einen anderen auf) und kein Benutzer beteiligt ist, kommt der Client Credentials Flow zum Einsatz. Dieser Flow ben\u00f6tigt kein PKCE und keine Redirect URI, da kein Authorization Code ausgetauscht wird. Der Client sendet Client-ID und Client-Secret direkt an den Token-Endpoint und erh\u00e4lt ein Access Token zur\u00fcck.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/services\/clientCredentials.js\nasync function getClientCredentialsToken(config) {\n  const body = new URLSearchParams({\n    grant_type: 'client_credentials',\n    client_id: config.clientId,\n    client_secret: config.clientSecret,\n    scope: config.scope || ''\n  });\n\n  const response = await fetch(config.tokenEndpoint, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application\/x-www-form-urlencoded'\n    },\n    body: body.toString()\n  });\n\n  if (!response.ok) {\n    const error = await response.json();\n    throw new Error(`Token-Request fehlgeschlagen: ${error.error_description || error.error}`);\n  }\n\n  const tokenData = await response.json();\n\n  return {\n    accessToken: tokenData.access_token,\n    tokenType: tokenData.token_type,\n    expiresIn: tokenData.expires_in, \/\/ Sekunden bis Ablauf\n    expiresAt: Date.now() + (tokenData.expires_in * 1000)\n  };\n}\n\n\/\/ Token-Cache um unn\u00f6tige Anfragen zu vermeiden\nlet cachedToken = null;\n\nasync function getValidToken(config) {\n  if (cachedToken && cachedToken.expiresAt > Date.now() + 60000) {\n    return cachedToken.accessToken; \/\/ 60 Sekunden Puffer vor Ablauf\n  }\n  cachedToken = await getClientCredentialsToken(config);\n  return cachedToken.accessToken;\n}\n\nmodule.exports = { getClientCredentialsToken, getValidToken };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Token-Cache verhindert unn\u00f6tige Anfragen an den Token-Endpoint. Das Token wird 60 Sekunden vor seinem Ablauf als ung\u00fcltig betrachtet (Puffer), damit kein Request mit einem fast-abgelaufenen Token starte. In Produktionsumgebungen mit mehreren Server-Instanzen sollte der Cache in Redis gespeichert werden, um Token-Endpoint-Anfragen \u00fcber alle Instanzen zu reduzieren.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ausgabe-beispiele\">Ausgabe-Beispiele: Erwartetes Verhalten<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nach vollst\u00e4ndiger Implementierung liefern die Endpoints folgende Ausgaben:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Erfolgreicher Login nach OAuth-Callback\nGET \/auth\/google\/callback?code=4\/P7q7W91a...&state=abc123\nHTTP 200 OK\nSet-Cookie: access_token=eyJhbGciOiJIUzI1NiJ9...; HttpOnly; SameSite=Lax\nSet-Cookie: refresh_token=eyJhbGciOiJIUzI1NiJ9...; HttpOnly; Path=\/auth\/refresh\n{\n  \"message\": \"Authentifizierung erfolgreich\",\n  \"user\": {\n    \"email\": \"benutzer@gmail.com\",\n    \"name\": \"Max Mustermann\"\n  }\n}\n\n# Gesch\u00fctzte Route mit g\u00fcltigem Token\nGET \/api\/profile\nCookie: access_token=eyJhbGciOiJIUzI1NiJ9...\nHTTP 200 OK\n{\n  \"userId\": \"116029571234567890\",\n  \"email\": \"benutzer@gmail.com\",\n  \"name\": \"Max Mustermann\"\n}\n\n# Gesch\u00fctzte Route ohne Token\nGET \/api\/profile\nHTTP 401 Unauthorized\n{ \"error\": \"Kein Token vorhanden\" }\n\n# Abgelaufenes Token\nGET \/api\/profile\nCookie: access_token=eyJhbGciOiJIUzI1NiJ9... (15 min vergangen)\nHTTP 401 Unauthorized\n{ \"error\": \"Token abgelaufen\", \"code\": \"TOKEN_EXPIRED\" }\n\n# Token-Refresh\nPOST \/auth\/refresh\nCookie: refresh_token=eyJhbGciOiJIUzI1NiJ9...\nHTTP 200 OK\nSet-Cookie: access_token=eyJhbGciOiJIUzI1NiJ9...(neu); HttpOnly\n{ \"message\": \"Token erfolgreich erneuert\" }<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"haeufige-fehler\">5 h\u00e4ufige Fehler bei der OAuth 2.0-Implementierung<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Fallstricke begegnen Entwicklern am h\u00e4ufigsten beim Implementieren von OAuth 2.0 in Node.js:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-1-tokens-in-localstorage-speichern\">Fehler 1: Tokens in localStorage speichern<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Viele Tutorials empfehlen, Access Tokens im Browser-localStorage oder sessionStorage zu speichern. Das ist gef\u00e4hrlich, weil JavaScript jeder Herkunft auf localStorage zugreifen kann. Ein einziger XSS-Angriff reicht, um alle gespeicherten Tokens zu stehlen, und der Angreifer kann die Tokens dann von einem eigenen Server aus verwenden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Tokens ausschlie\u00dflich als <code>httpOnly<\/code>-Cookies setzen. JavaScript kann auf httpOnly-Cookies grunds\u00e4tzlich nicht zugreifen, auch nicht bei XSS. Die Cookie-Konfiguration in diesem Tutorial zeigt den korrekten Ansatz. Das Muster mit <a href=\"\/at\/two-factor-authentication-nodejs\/\">Two-Factor Authentication<\/a> kombiniert erh\u00f6ht die Sicherheit weiter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-2-state-parameter-weglassen\">Fehler 2: State-Parameter weglassen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Ohne den <code>state<\/code>-Parameter ist der OAuth-Flow anf\u00e4llig f\u00fcr CSRF-Angriffe. Ein Angreifer k\u00f6nnte einen Benutzer dazu bringen, einen Authorization Code zu aktivieren, der dann an den Angreifer-Account gebunden wird (&#8220;Login CSRF&#8221;). Das ist ein reales Angriffsszenario mit dokumentierten Vorf\u00e4llen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Immer einen kryptographisch zuf\u00e4lligen <code>state<\/code>-Parameter im Authorization Request mitsenden und im Callback gegen den gespeicherten Wert verifizieren. Passport.js \u00fcbernimmt das automatisch mit <code>state: true<\/code>. Bei custom Implementierungen muss das explizit mit <code>crypto.randomBytes(16).toString('hex')<\/code> generiert werden.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-3-redirect-uri-zu-grosszuegig-konfigurieren\">Fehler 3: Redirect URI zu gro\u00dfz\u00fcgig konfigurieren<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Manche Entwickler registrieren Wildcards als Redirect URI (<code>https:\/\/app.com\/*<\/code>) oder verwenden Regex-Matching, wenn der Authorization Server das anbietet. Das erm\u00f6glicht es einem Angreifer, den Authorization Code an eine beliebige URL unter der Domain umzuleiten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Redirect URIs immer als exakte Strings registrieren und aus Umgebungsvariablen laden. F\u00fcr Entwicklung und Produktion separate, exakte URIs in der Providerkonsole eintragen. Der Authorization Server soll String-Matching ohne Pattern-Unterst\u00fctzung erzwingen, wie es OAuth 2.1 vorschreibt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-4-google-tokens-direkt-als-app-tokens-verwenden\">Fehler 4: Google-Tokens direkt als App-Tokens verwenden<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Der von Google erhaltene Access Token wird direkt an den Browser weitergegeben und f\u00fcr API-Anfragen verwendet. Das erzeugt eine starke Abh\u00e4ngigkeit von Google, und der Token hat Googles eigene Laufzeit und Scopes, nicht die der App. Au\u00dferdem kann der Token nicht kontrolliert revoziert werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Nach erfolgreichem OAuth-Handshake eigene JWTs ausstellen (wie in Schritt 7 gezeigt). Der Google-Token wird nur beim Login verwendet, um die Identit\u00e4t zu pr\u00fcfen. Danach arbeitet die App ausschlie\u00dflich mit eigenen Tokens, die volle Kontrolle \u00fcber Laufzeit, Scopes und Revokation bieten.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fehler-5-refresh-token-fuer-jeden-benutzer-mehrfach-ausstellen\">Fehler 5: Refresh Token f\u00fcr jeden Benutzer mehrfach ausstellen<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem:<\/strong> Wenn bei jedem Login ein neuer Refresh Token ausgestellt wird ohne den alten zu invalidieren, k\u00f6nnen sich im Laufe der Zeit viele g\u00fcltige Refresh Tokens f\u00fcr denselben Benutzer ansammeln. Ein gestohlener Token bleibt dauerhaft g\u00fcltig, selbst wenn sich der Benutzer neu anmeldet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>L\u00f6sung:<\/strong> Refresh Token Rotation implementieren: Bei jedem Einsatz eines Refresh Tokens wird ein neues Refresh Token ausgestellt und das alte in der Blocklist gespeichert. Wenn ein bereits invalidierter Refresh Token verwendet wird, deutet das auf Token-Diebstahl hin, und alle Sessions des Benutzers werden gesperrt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"weitere-fallstricke\">Weitere Pitfalls auf einen Blick<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fehlermeldung \/ Problem<\/th><th>Ursache<\/th><th>L\u00f6sung<\/th><\/tr><\/thead><tbody><tr><td><code>redirect_uri_mismatch<\/code><\/td><td>URI stimmt nicht exakt \u00fcberein<\/td><td>Trailing Slash, HTTP vs HTTPS, genaue URI in Konsole pr\u00fcfen<\/td><\/tr><tr><td><code>invalid_grant<\/code><\/td><td>Authorization Code bereits verwendet<\/td><td>Code ist einmalig, Seite neu laden und Flow neu starten<\/td><\/tr><tr><td>PKCE <code>invalid_code_verifier<\/code><\/td><td>Falsches Encoding des Verifiers<\/td><td><code>base64url<\/code> verwenden, nicht <code>base64<\/code> (kein =, +, \/)<\/td><\/tr><tr><td>Session nach Callback leer<\/td><td>Cookie-Attribute verhindern Session<\/td><td><code>sameSite: 'lax'<\/code> und <code>secure<\/code>-Flag in Produktion pr\u00fcfen<\/td><\/tr><tr><td>Google gibt keinen Refresh Token<\/td><td><code>prompt: 'consent'<\/code> fehlt<\/td><td><code>accessType: 'offline'<\/code> + <code>prompt: 'consent'<\/code> setzen<\/td><\/tr><tr><td><code>passport.session()<\/code> Error<\/td><td>Falsche Middleware-Reihenfolge<\/td><td><code>session()<\/code> vor <code>passport.initialize()<\/code> vor <code>passport.session()<\/code><\/td><\/tr><tr><td>Token nicht in Cookie<\/td><td><code>cookie-parser<\/code> fehlt<\/td><td><code>app.use(cookieParser())<\/code> vor Route-Middleware setzen<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting\">Troubleshooting: 8 h\u00e4ufige Fehlermeldungen erkl\u00e4rt<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Error 400: redirect_uri_mismatch<\/strong> &#8211; Die Redirect URI im Code stimmt nicht exakt mit der in der Google Console registrierten URI \u00fcberein. Leerzeichen, abschlie\u00dfende Slashes und HTTP vs. HTTPS pr\u00fcfen. Umgebungsvariable <code>GOOGLE_CALLBACK_URL<\/code> in der Konsole und im <code>.env<\/code> vergleichen.<\/li>\n<li><strong>Error 401: invalid_client<\/strong> &#8211; Client-ID oder Client-Secret ist falsch. Die <code>.env<\/code>-Datei pr\u00fcfen und sicherstellen, dass <code>require('dotenv').config()<\/code> die erste Zeile in <code>app.js<\/code> ist, bevor andere Module geladen werden.<\/li>\n<li><strong>Error 400: invalid_grant<\/strong> &#8211; Der Authorization Code wurde bereits eingel\u00f6st oder ist abgelaufen (\u00fcblicherweise 10 Minuten G\u00fcltigkeitsfenster). Seite neu laden und OAuth-Flow vollst\u00e4ndig neu starten.<\/li>\n<li><strong>Error 403: access_denied<\/strong> &#8211; Der Benutzer hat die Zustimmung verweigert, oder die angeforderten Scopes sind f\u00fcr die App nicht verf\u00fcgbar. Scopes in der Google Console pr\u00fcfen und ggf. die OAuth-Zustimmungsseite korrekt konfigurieren.<\/li>\n<li><strong>TokenExpiredError: jwt expired<\/strong> &#8211; Das JWT-Access-Token ist abgelaufen. Das ist erwartetes Verhalten. Der Client soll automatisch den <code>\/auth\/refresh<\/code>-Endpoint aufrufen, wenn <code>code: 'TOKEN_EXPIRED'<\/code> in der Antwort steht.<\/li>\n<li><strong>JsonWebTokenError: invalid signature<\/strong> &#8211; Der JWT-Secret stimmt nicht mit dem Secret \u00fcberein, mit dem das Token ausgestellt wurde. Alle ausgestellten Tokens werden sofort ung\u00fcltig, wenn der Secret ge\u00e4ndert wird. Secret niemals ohne Planung \u00e4ndern.<\/li>\n<li><strong>Error: Failed to serialize user into session<\/strong> &#8211; Passport&#8217;s <code>serializeUser<\/code> ist nicht korrekt konfiguriert oder wurde nicht registriert. Pr\u00fcfen, ob <code>passport.serializeUser<\/code> und <code>passport.deserializeUser<\/code> in der Passport-Konfiguration vorhanden sind.<\/li>\n<li><strong>ECONNREFUSED redis:\/\/localhost:6379<\/strong> &#8211; Redis l\u00e4uft nicht. Mit <code>redis-cli ping<\/code> pr\u00fcfen (Antwort: <code>PONG<\/code>). F\u00fcr lokale Entwicklung Redis starten: <code>docker run -d -p 6379:6379 redis:alpine<\/code>.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sicherheits-checkliste\">OAuth 2.0 Sicherheits-Checkliste vor dem Go-Live<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vor dem Produktionseinsatz jede OAuth 2.0-Implementierung anhand dieser Checkliste pr\u00fcfen. Jeder Punkt adressiert ein reales Sicherheitsrisiko:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Pr\u00fcfpunkt<\/th><th>Wichtigkeit<\/th><th>Betrifft<\/th><\/tr><\/thead><tbody><tr><td>PKCE f\u00fcr alle Authorization Code Flows<\/td><td>Kritisch<\/td><td>Code-Injection-Schutz<\/td><\/tr><tr><td>State-Parameter f\u00fcr CSRF-Schutz<\/td><td>Kritisch<\/td><td>Login-CSRF-Schutz<\/td><\/tr><tr><td>httpOnly Cookies statt localStorage<\/td><td>Kritisch<\/td><td>XSS-Token-Diebstahl<\/td><\/tr><tr><td>Redirect URI exakt registriert (kein Wildcard)<\/td><td>Kritisch<\/td><td>Open-Redirect-Angriffe<\/td><\/tr><tr><td>Access Token Laufzeit max. 15 Minuten<\/td><td>Hoch<\/td><td>Token-Kompromittierung<\/td><\/tr><tr><td>Refresh Token Rotation implementiert<\/td><td>Hoch<\/td><td>Replay-Angriffe auf Refresh Tokens<\/td><\/tr><tr><td>Token-Blocklist f\u00fcr sofortige Revokation<\/td><td>Hoch<\/td><td>Kompromittierte Token invalidieren<\/td><\/tr><tr><td>Issuer-Validierung beim JWT-Verify<\/td><td>Hoch<\/td><td>Token-Confusion-Angriffe<\/td><\/tr><tr><td>Rate Limiting auf Auth-Endpoints<\/td><td>Mittel<\/td><td>Brute-Force-Angriffe<\/td><\/tr><tr><td>Minimale Scopes angefordert<\/td><td>Mittel<\/td><td>Minimierung des Angriffsradius<\/td><\/tr><tr><td>Tokens nicht in Server-Logs<\/td><td>Mittel<\/td><td>Token-Leak durch Logs<\/td><\/tr><tr><td>HTTPS in Produktion erzwungen<\/td><td>Kritisch<\/td><td>Man-in-the-Middle-Angriffe<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fortgeschrittene-tipps\">Fortgeschrittene Tipps f\u00fcr Produktionsumgebungen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wer die Implementierung produktionsreif machen will, sollte folgende Aspekte beachten:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"rate-limiting-fuer-auth-endpoints\">Rate Limiting f\u00fcr Auth-Endpoints<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth-Endpoints sind bevorzugte Ziele f\u00fcr automatisierte Angriffe. Der <code>\/auth\/refresh<\/code>-Endpoint sollte auf maximal 10 Anfragen pro Minute pro IP begrenzt werden. Der <code>\/auth\/logout<\/code>-Endpoint auf 5 Anfragen pro Minute. Das <a href=\"\/at\/rate-limiting-nodejs\/\">Rate Limiting in Node.js Tutorial<\/a> zeigt die Implementierung mit <code>express-rate-limit<\/code> und Redis-Backing f\u00fcr verteilte Systeme.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"dpop-demonstrating-proof-of-possession\">DPoP (Demonstrating Proof of Possession)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bearer-Tokens k\u00f6nnen von jedem verwendet werden, der sie besitzt. DPoP ist eine Erweiterung, die ein Access Token an den Client bindet, der es angefordert hat. Bei jeder API-Anfrage signiert der Client einen kleinen Proof-of-Possession-JWT mit seinem privaten Schl\u00fcssel. Der Server verifiziert diese Signatur und stellt sicher, dass der Absender derselbe Client ist, der das Token angefordert hat. Gestohlene Bearer-Tokens sind damit deutlich weniger wertvoll. Mehr \u00fcber digitale Signaturen erkl\u00e4rt das Tutorial zu <a href=\"\/at\/digitale-signatur-nodejs\/\">Digitale Signaturen in Node.js<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"monitoring-und-anomalie-erkennung\">Monitoring und Anomalie-Erkennung<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In Produktion sollten Auth-Endpoints auf Anomalien \u00fcberwacht werden: ungew\u00f6hnlich viele fehlgeschlagene Login-Versuche, Token-Refreshes aus unbekannten IP-Adressen, oder Verwendung von bereits revozierten Tokens (deutet auf aktiven Angriff hin). Tools wie Prometheus und Grafana lassen sich direkt in Express-Apps integrieren. Jede Verwendung eines revozierten Tokens sollte einen Alert ausl\u00f6sen. Das Thema Zwei-Faktor-Authentifizierung als zus\u00e4tzliche Schutzschicht behandelt das Tutorial zu <a href=\"\/at\/two-factor-authentication-nodejs\/\">Two-Factor Authentication in Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"oauth-21-ausblick\">OAuth 2.1: Was sich gegen\u00fcber OAuth 2.0 \u00e4ndert<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.1 konsolidiert OAuth 2.0 und seine wichtigsten Sicherheitserweiterungen (PKCE, Token Rotation, Sender-Constrained Tokens) in einem einzigen Dokument. Entwickler, die heute mit PKCE und httpOnly-Cookies implementieren, sind bereits weitgehend OAuth 2.1-kompatibel. Die konkreten \u00c4nderungen:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>PKCE ist Pflicht<\/strong> f\u00fcr alle Authorization Code Flows, auch f\u00fcr vertrauliche (server-seitige) Clients<\/li>\n<li><strong>Implicit Flow ist entfernt<\/strong> aus der Spezifikation, da er inherent unsicher ist<\/li>\n<li><strong>ROPC-Flow (Resource Owner Password Credentials) ist entfernt<\/strong>, da er das Passwort an den Client weitergibt<\/li>\n<li><strong>Refresh Token Rotation ist empfohlen<\/strong> f\u00fcr alle public clients<\/li>\n<li><strong>Redirect URIs m\u00fcssen exakt matchen<\/strong>, Wildcard-Matching ist explizit verboten<\/li>\n<li><strong>Bearer-Token-\u00dcbertragung in URLs ist verboten<\/strong>, nur Header oder httpOnly-Cookies<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Die <a href=\"https:\/\/oauth.net\/2\/\" target=\"_blank\" rel=\"noopener noreferrer\">offizielle OAuth 2.0-Website<\/a> und die zugeh\u00f6rigen IETF-RFCs sind der ma\u00dfgebliche Referenzpunkt f\u00fcr alle Implementierungsdetails und Sicherheitsrichtlinien.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"verwandte-artikel\">Verwandte Artikel<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"weiterfuehrende-lektuere\">Weiterf\u00fchrende Lekt\u00fcre<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/at\/jwt-authentication-nodejs\/\">JWT Authentication in Node.js: 10 Schritte<\/a> &#8211; Token-basierte Authentifizierung als Grundlage<\/li>\n<li><a href=\"\/at\/two-factor-authentication-nodejs\/\">Two-Factor Authentication in Node.js: 11 Schritte<\/a> &#8211; OAuth mit 2FA f\u00fcr maximale Sicherheit kombinieren<\/li>\n<li><a href=\"\/at\/csrf-protection-nodejs\/\">CSRF Protection in Node.js: 12 Schritte<\/a> &#8211; CSRF-Angriffe \u00fcber OAuth hinaus abwehren<\/li>\n<li><a href=\"\/at\/nodejs-session-management\/\">Node.js Session Management: 11 Schritte<\/a> &#8211; Session-basierte Alternative zu JWT<\/li>\n<li><a href=\"\/at\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: 12 Schritte<\/a> &#8211; Auth-Endpoints vor Missbrauch sch\u00fctzen<\/li>\n<li><a href=\"\/at\/digitale-signatur-nodejs\/\">Digitale Signatur in Node.js: 11 Schritte<\/a> &#8211; Kryptographische Grundlagen f\u00fcr Token-Signierung<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">FAQ: OAuth 2.0 in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"was-ist-der-unterschied-zwischen-oauth-2-0-und-openid-connect\">Was ist der Unterschied zwischen OAuth 2.0 und OpenID Connect?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 ist ein Autorisierungsprotokoll, das regelt, welche Ressourcen eine App im Auftrag eines Benutzers zugreifen darf. OpenID Connect (OIDC) ist eine Identit\u00e4tsschicht auf OAuth 2.0, die Authentifizierung hinzuf\u00fcgt, also die Identit\u00e4tspr\u00fcfung des Benutzers. OIDC liefert neben dem Access Token auch ein ID Token (JWT) mit Benutzerinformationen wie E-Mail und Name. In der Praxis werden beide kombiniert: OAuth 2.0 f\u00fcr die Ressourcen-Autorisierung, OIDC f\u00fcr den Login und die Benutzerprofil-\u00dcbertragung.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"warum-ist-pkce-auch-fuer-server-seitige-apps-empfohlen\">Warum ist PKCE auch f\u00fcr server-seitige Apps empfohlen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Urspr\u00fcnglich wurde PKCE nur f\u00fcr public clients (Browser-Apps, Native Apps) entwickelt, die kein Client-Secret sicher speichern k\u00f6nnen. Die OAuth 2.1-Spezifikation empfiehlt PKCE aber auch f\u00fcr vertrauliche (server-seitige) Clients, weil es eine zus\u00e4tzliche Schutzschicht gegen Authorization Code Interception bietet. Ein abgefangener Authorization Code ist ohne den <code>code_verifier<\/code> wertlos, selbst wenn der Angreifer auch das Client-Secret kennt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"wie-lange-sollten-access-tokens-gueltig-sein\">Wie lange sollten Access Tokens g\u00fcltig sein?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die allgemeine Empfehlung liegt bei 5-15 Minuten f\u00fcr Access Tokens. Kurze Laufzeiten begrenzen den Schaden erheblich, wenn ein Token kompromittiert wird. Refresh Tokens k\u00f6nnen 7-30 Tage g\u00fcltig sein, sollten aber mit Token Rotation kombiniert werden. Bei besonders sensiblen Anwendungen (Banking, Gesundheit) werden Access Tokens oft auf 5 Minuten oder weniger begrenzt. Die Wahl der Laufzeit ist ein Kompromiss zwischen Sicherheit und Nutzerkomfort.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"was-passiert-wenn-jemand-den-authorization-code-abfaengt\">Was passiert, wenn jemand den Authorization Code abf\u00e4ngt?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ohne PKCE kann ein abgefangener Authorization Code direkt gegen ein Access Token eingetauscht werden. Mit PKCE ist das nicht m\u00f6glich, weil der Angreifer den <code>code_verifier<\/code> nicht kennt, der auf dem Client gespeichert ist und f\u00fcr den Token-Request ben\u00f6tigt wird. Der Authorization Code selbst ist ohne den passenden Verifier wertlos. Das ist der Kernvorteil von PKCE und der Grund, warum es f\u00fcr alle Flows empfohlen wird.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kann-oauth-2-0-ohne-externe-identity-provider-implementiert-werden\">Kann OAuth 2.0 ohne externe Identity Provider implementiert werden?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. Ein eigener Authorization Server kann mit Node.js-Paketen wie <code>@node-oauth\/oauth2-server<\/code> oder <code>oidc-provider<\/code> implementiert werden. Das ist jedoch erheblich komplexer und sicherheitskritischer als die Nutzung von Google, GitHub oder einem Identity-as-a-Service-Anbieter. Fehler in eigenen Authorization-Server-Implementierungen k\u00f6nnen schwerwiegende Sicherheitsl\u00fccken erzeugen. F\u00fcr die meisten Anwendungen ist ein externer Provider die sicherere und wartungs\u00e4rmere Wahl.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welche-owasp-sicherheitsrisiken-adressiert-eine-korrekte-oauth-2-0-implementierung\">Welche OWASP-Sicherheitsrisiken adressiert eine korrekte OAuth 2.0-Implementierung?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Eine korrekte OAuth 2.0-Implementierung adressiert direkt mehrere <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Top 10<\/a>-Risiken: A01 (Broken Access Control) durch korrekte Token-Validierung, A02 (Cryptographic Failures) durch sichere Token-Signierung mit starken Secrets, A07 (Identification and Authentication Failures) durch State-Parameter und PKCE sowie A10 (Server-Side Request Forgery) durch exakte Redirect-URI-Validierung. Die Kombination aller Ma\u00dfnahmen in diesem Tutorial deckt diese Risiken vollst\u00e4ndig ab.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"wie-wird-oauth-2-0-mit-csrf-schutz-kombiniert\">Wie wird OAuth 2.0 mit CSRF-Schutz kombiniert?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 enth\u00e4lt bereits einen integrierten CSRF-Schutz durch den <code>state<\/code>-Parameter. Dar\u00fcber hinaus sch\u00fctzt das <code>sameSite: 'lax'<\/code>-Cookie-Attribut vor CSRF-Angriffen auf die eigene App. F\u00fcr vollst\u00e4ndigen CSRF-Schutz auf nicht-OAuth-Routen (Formulare, API-Endpoints) sollte zus\u00e4tzlich das <code>csurf<\/code>-Muster oder das Double-Submit-Cookie-Muster eingesetzt werden. Das <a href=\"\/at\/csrf-protection-nodejs\/\">CSRF Protection in Node.js Tutorial<\/a> behandelt dieses Thema ausf\u00fchrlich.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"welches-passport-paket-ist-2026-empfohlen\">Welches Passport-Paket ist 2026 empfohlen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr Google OAuth 2.0 in Node.js ist <code>passport-google-oauth20<\/code> die meistgenutzte Wahl. F\u00fcr andere OpenID Connect-Provider gibt es <code>passport-openidconnect<\/code>. Wer mehr Flexibilit\u00e4t und PKCE-Unterst\u00fctzung braucht, greift auf <code>openid-client<\/code> zur\u00fcck, das als OpenID-zertifizierte Bibliothek gilt. Die <a href=\"https:\/\/www.passportjs.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">offizielle Passport.js-Website<\/a> listet alle verf\u00fcgbaren Strategien. Wer einen eigenen Authorization Server baut, nutzt <code>@node-oauth\/oauth2-server<\/code> oder <code>oidc-provider<\/code>.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>OAuth 2.0 ist das meistgenutzte Autorisierungsprotokoll im Web. \u00dcber 90 Prozent aller modernen Web-APIs setzen auf OAuth 2.0 als Grundlage. Wer eine Node.js-Anwendung absichern will, die sich mit Google, GitHub\u2026<\/p>\n","protected":false},"author":4,"featured_media":120,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-119","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/119","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/comments?post=119"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/119\/revisions"}],"predecessor-version":[{"id":196,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/posts\/119\/revisions\/196"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media\/120"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/media?parent=119"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/categories?post=119"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/at\/wp-json\/wp\/v2\/tags?post=119"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}