🔐 Was ist OAuth 2.0?
📖 Definition
OAuth 2.0 ist ein offener Standardprotokoll, das es Benutzern ermöglicht, anderen Websites oder Anwendungen Zugriff auf ihre Informationen zu gewähren, ohne ihr Passwort zu teilen. Repräsentative Beispiele sind Social Logins wie "Mit Google anmelden" oder "Mit GitHub anmelden". Es konzentriert sich auf Autorisierung (Authorization) anstatt Authentifizierung (Authentication) und bietet eine sichere und bequeme Benutzererfahrung.
🎯 Verstehen durch Analogien
Hotel-Schlüsselkarte
Traditionelle Methode = Hotel-Generalschlüssel
Hotelbesitzer: "Ich gebe Ihnen den Generalschlüssel"
Sie: "Aber der öffnet alle Zimmer! 😱"
"Das ist wie mein Passwort zu teilen"
OAuth 2.0 = Hotel-Schlüsselkarte
Hotelbesitzer: "Diese Schlüsselkarte..."
✅ Öffnet nur Ihr Zimmer
✅ Läuft automatisch beim Checkout ab
✅ Bei Verlust ist der Generalschlüssel sicher
✅ Sie können sie jederzeit ungültig machen
Sie: "Perfekt!"
Vollmacht
Szenario: Bankgeschäfte an einen Vertreter delegieren
❌ Passwort-Sharing-Methode
Sie: "Mein Bankpasswort ist 1234"
Vertreter: "Ich kann alles bei der Bank tun!"
Risiko: Alle Rechte wie Geld abheben, Konten löschen usw.
✅ OAuth-Methode
Sie: "Ich gehe zur Bank und erstelle eine Vollmacht"
Bank: "Welche Berechtigungen möchten Sie erteilen?"
Sie: "Nur Kontostand abfragen"
Bank: "Für wie lange?"
Sie: "Nur eine Woche"
Der Vertreter kann:
✅ Nur Kontostand abfragen
✅ Läuft automatisch nach einer Woche ab
✅ Kennt Ihr Passwort nicht
✅ Berechtigung kann jederzeit widerrufen werden
⚙️ Wie es funktioniert
1. OAuth 2.0 Schlüsselkonzepte
┌─────────────────────────────────────────┐
│ OAuth 2.0 4 Rollen │
└─────────────────────────────────────────┘
1. Resource Owner (Ressourcenbesitzer)
└─ Benutzer (Sie)
2. Client (Klient)
└─ Anwendung, die den Dienst nutzen möchte
└─ Beispiel: Ihre Website
3. Authorization Server (Autorisierungsserver)
└─ Server, der Berechtigungen erteilt
└─ Beispiel: Google, GitHub, Naver
4. Resource Server (Ressourcenserver)
└─ Server, der Benutzerinformationen enthält
└─ Beispiel: Google API, GitHub API
2. OAuth 2.0 Authentifizierungsfluss (Authorization Code)
┌──────────┐ ┌──────────┐
│ Benutzer │ │ Client │
│ (Sie) │ │ (webapp) │
└─────┬────┘ └────┬─────┘
│ │
│ 1. Klick auf "Mit Google anmelden" │
│──────────────────────────────────────────>│
│ │
│ 2. Weiterleitung zu Google-Anmeldeseite │
│<──────────────────────────────────────────│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ 3. Login│ (Auth Server) │ │
│────────>│ │ │
│ │ │ │
│ 4. Berechtigungsanfrage │
│ "Diese App möchte Ihre │
│ E-Mail und Profil sehen" │
│<────────│ │ │
│ │ │ │
│ 5. Genehmigen │
│────────>│ │ │
│ │ │ │
│ │ 6. Authorization Code ausgeben │
│ │ (einmaliger Code) │
│<────────│ │ │
│ └──────────────────┘ │
│ │
│ 7. Authorization Code senden │
│──────────────────────────────────────────>│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ │ (Auth Server) │ 8. Austausch│
│ │ │<─── Code + │
│ │ │ Client │
│ │ │ Secret │
│ │ │ │
│ │ 9. Access Token ausgeben ──────>│
│ │ │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Google API │ │
│ │ (Resource │ 10. Benutzer│
│ │ Server) │<──── info mit│
│ │ │ Token │
│ │ │ anfordern│
│ │ 11. Benutzerinfo │ ────────────>│
│ │ Antwort │ │
│ └──────────────────┘ │
│ │
│ 12. Anmeldung abgeschlossen! │
│<─────────────────── ───────────────────────│
│ │
Kernpunkte:
- Benutzerpasswort wird nur bei Google eingegeben
- Die Webapp kann das Passwort nie erfahren
- Greift nur mit Access Token auf autorisierte Informationen zu
3. OAuth 2.0 Grant Types
========== 1. Authorization Code (am sichersten) ==========
Verwendung: Serverseitige Webanwendungen
Vorteile: Am sichersten (verwendet Client Secret)
Beispiel: "Mit Google anmelden"
Ablauf:
1. Benutzer → Google-Anmeldung
2. Google → Authorization Code ausgeben
3. Server → Code + Secret gegen Access Token tauschen
4. Server → API mit Token aufrufen
========== 2. Implicit (einfach aber weniger sicher) ==========
Verwendung: Nur-Browser-Apps (SPA)
Vorteile: Einfach (kein Server erforderlich)
Nachteile: Sicherheitsanfällig (Token in URL sichtbar)
Beispiel: Alte SPAs
Ablauf:
1. Benutzer → Google-Anmeldung
2. Google → Access Token direkt ausgeben (in URL)
3. Browser → API mit Token aufrufen
⚠️ Derzeit nicht empfohlen
========== 3. Resource Owner Password (Legacy) ==========
Verwendung: Nur vertrauenswürdige Apps
Vorteile: Einfach
Nachteile: App erfährt Benutzerpasswort
Beispiel: Eigene mobile App
Ablauf:
1. App → Erhält Benutzer-ID/Passwort
2. App → Sendet an Authorization Server
3. Authorization Server → Gibt Access Token aus
⚠️ Widerspricht OAuth-Philosophie, nicht empfohlen
========== 4. Client Credentials ==========
Verwendung: Server-zu-Server-Kommunikation (M2M)
Vorteile: Keine Benutzerauthentifizierung erforderlich
Beispiel: Backend-Service-Kommunikation
Ablauf:
1. Server → Sendet Client ID + Secret
2. Authorization Server → Gibt Access Token aus
3. Server → Ruft API mit Token auf
========== 5. PKCE (Proof Key for Code Exchange) ==========
Verwendung: Mobile Apps, SPA (derzeit empfohlen)
Vorteile: Authorization Code + zusätzliche Sicherheit
Beispiel: Moderne mobile/SPA-Apps
Ablauf:
1. App → Generiert code_verifier (Zufallszeichenkette)
2. App → Generiert code_challenge (Verifier-Hash)
3. App → Fordert Authorization Code mit Challenge an
4. Google → Gibt Code aus
5. App → Tauscht Code + Verifier gegen Token
6. Google → Verifiziert Verifier und gibt Token aus
💡 Echte Beispiele
Google OAuth 2.0 Anmeldung (Node.js)
// ========== 1. Google Cloud Console Einrichtung ==========
/*
1. Besuchen Sie https://console.cloud.google.com
2. Projekt erstellen
3. "APIs & Dienste" → "Anmeldedaten"
4. "OAuth 2.0-Client-ID" erstellen
5. Autorisierte Weiterleitungs-URI hinzufügen:
http://localhost:3000/auth/google/callback
6. Client ID und Client Secret erhalten
*/
// ========== 2. Server-Implementierung ==========
const express = require('express');
const axios = require('axios');
const session = require('express-session');
const app = express();
// Sitzungskonfiguration
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));
// Google OAuth-Konfiguration
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';
// ========== Schritt 1: Anmeldung starten ==========
app.get('/auth/google', (req, res) => {
// Google-Authentifizierungs-URL generieren
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code', // Authorization Code anfordern
scope: 'openid email profile', // Anzufordernde Berechtigungen
access_type: 'offline', // Refresh Token erhalten
prompt: 'consent' // Zustimmungsbildschirm immer anzeigen
});
// Zu Google-Anmeldeseite weiterleiten
res.redirect(`${authUrl}?${params}`);
});
// ========== Schritt 2: Callback verarbeiten ==========
app.get('/auth/google/callback', async (req, res) => {
const { code, error } = req.query;
// Wenn Benutzer ablehnt
if (error) {
return res.status(400).send(`Authentifizierung fehlgeschlagen: ${error}`);
}
try {
// Authorization Code gegen Access Token tauschen
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}
);
const {
access_token,
refresh_token,
expires_in,
token_type
} = tokenResponse.data;
console.log('Access Token:', access_token);
console.log('Läuft ab in:', expires_in, 'Sekunden');
// Benutzerinformationen mit Access Token abrufen
const userResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`
}
}
);
const user = userResponse.data;
/*
{
id: "123456789",
email: "user@gmail.com",
verified_email: true,
name: "Max Müller",
picture: "https://lh3.googleusercontent.com/...",
locale: "de"
}
*/
// Benutzerinformationen in Sitzung speichern
req.session.user = {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
accessToken: access_token,
refreshToken: refresh_token
};
// Benutzer in Datenbank speichern
await saveOrUpdateUser(user);
// Zur Hauptseite weiterleiten
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth-Fehler:', error.response?.data || error.message);
res.status(500).send('Fehler bei der Authentifizierungsverarbeitung');
}
});
// ========== Benutzerinformationen überprüfen ==========
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}
res.send(`
<h1>Hallo, ${req.session.user.name}!</h1>
<img src="${req.session.user.picture}" alt="Profil" />
<p>E-Mail: ${req.session.user.email}</p>
<a href="/logout">Abmelden</a>
`);
});
// ========== Abmelden ==========
app.get('/logout', async (req, res) => {
// Google Access Token widerrufen (optional)
const accessToken = req.session.user?.accessToken;
if (accessToken) {
try {
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
} catch (error) {
console.error('Token-Widerruf fehlgeschlagen:', error.message);
}
}
// Sitzung löschen
req.session.destroy();
res.redirect('/');
});
// ========== Startseite ==========
app.get('/', (req, res) => {
res.send(`
<h1>OAuth 2.0 Demo</h1>
<a href="/auth/google">
<button>Mit Google anmelden</button>
</a>
`);
});
app.listen(3000, () => {
console.log('Server läuft: http://localhost:3000');
});
Einfache Implementierung mit Passport.js
// ========== Passport.js verwenden ==========
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Passport-Konfiguration
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
// Benutzerinformationen verarbeiten
try {
// Benutzer in Datenbank suchen oder erstellen
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
picture: profile.photos[0].value
});
}
done(null, user);
} catch (error) {
done(error, null);
}
}
));
// Sitzungsserialisierung
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
// Express-Konfiguration
app.use(passport.initialize());
app.use(passport.session());
// ========== Routen ==========
// Google-Anmeldung starten
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Google-Callback
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);
// Abmelden
app.get('/logout', (req, res) => {
req.logout((err) => {
if (err) console.error(err);
res.redirect('/');
});
});
// Authentifizierungs-Middleware
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
// Geschützte Route
app.get('/dashboard', isAuthenticated, (req, res) => {
res.send(`Hallo, ${req.user.name}!`);
});
GitHub OAuth 2.0
// ========== GitHub OAuth-Einrichtung ==========
/*
1. Besuchen Sie https://github.com/settings/developers
2. Klicken Sie auf "New OAuth App"
3. Informationen eingeben:
- Application name: My App
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
4. Client ID und Client Secret erhalten
*/
const express = require('express');
const axios = require('axios');
const app = express();
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';
// ========== GitHub-Anmeldung ==========
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email', // Anzufordernde Berechtigungen
state: Math.random().toString(36) // CSRF-Schutz
});
res.redirect(`${authUrl}?${params}`);
});
// ========== Callback verarbeiten ==========
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
try {
// 1. Access Token erhalten
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);
const { access_token } = tokenResponse.data;
// 2. Benutzerinformationen abrufen
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`
}
});
const user = userResponse.data;
/*
{
login: "username",
id: 12345,
avatar_url: "https://avatars.githubusercontent.com/...",
name: "Max Müller",
email: "user@example.com",
bio: "Ich bin Entwickler",
public_repos: 50
}
*/
// 3. E-Mail abrufen (separate Anfrage)
const emailResponse = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`
}
});
const primaryEmail = emailResponse.data.find(email => email.primary);
res.json({
user,
email: primaryEmail.email
});
} catch (error) {
console.error('GitHub OAuth-Fehler:', error.message);
res.status(500).send('Authentifizierung fehlgeschlagen');
}
});
app.listen(3000);
OAuth in React verwenden
// ========== React-Client ==========
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function App() {
const [user, setUser] = useState(null);
// Benutzerinformationen beim Laden der Seite überprüfen
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
try {
const response = await axios.get('/api/me', {
withCredentials: true // Cookies einbeziehen
});
setUser(response.data);
} catch (error) {
console.log('Nicht angemeldet');
}
}
function handleGoogleLogin() {
// Zum OAuth-Endpunkt des Servers gehen
window.location.href = 'http://localhost:3000/auth/google';
}
function handleGitHubLogin() {
window.location.href = 'http://localhost:3000/auth/github';
}
function handleLogout() {
window.location.href = 'http://localhost:3000/logout';
}
if (user) {
return (
<div>
<h1>Hallo, {user.name}!</h1>
<img src={user.picture} alt="Profil" />
<p>{user.email}</p>
<button onClick={handleLogout}>Abmelden</button>
</div>
);
}
return (
<div>
<h1>Anmelden</h1>
<button onClick={handleGoogleLogin}>
🔵 Mit Google anmelden
</button>
<button onClick={handleGitHubLogin}>
⚫ Mit GitHub anmelden
</button>
</div>
);
}
export default App;
Sicherheit mit PKCE verbessern (SPA/Mobil)
// ========== PKCE (Proof Key for Code Exchange) ==========
// Für mobile Apps oder SPAs
const crypto = require('crypto');
// ========== 1. Code Verifier generieren ==========
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}
// ========== 2. Code Challenge generieren ==========
function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256')
.update(verifier)
.digest();
return base64URLEncode(hash);
}
function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// ========== 3. PKCE-Anmeldung starten (Client) ==========
async function startPKCELogin() {
// Verifier generieren und speichern
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);
// Challenge generieren
const codeChallenge = generateCodeChallenge(codeVerifier);
// Google-Authentifizierungs-URL
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge, // PKCE
code_challenge_method: 'S256' // SHA-256
});
window.location.href = `${authUrl}?${params}`;
}
// ========== 4. Callback verarbeiten ==========
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
// Gespeicherten Verifier abrufen
const codeVerifier = sessionStorage.getItem('code_verifier');
// Token tauschen
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // PKCE Verifier
})
});
const { access_token } = await response.json();
// Verifier löschen
sessionStorage.removeItem('code_verifier');
return access_token;
}
🤔 Häufig gestellte Fragen
F1. OAuth vs JWT vs Session?
A:
// ========== 1. Session (Sitzung) ==========
// Traditionelle Methode
// Anmeldung
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Authentifizierung überprüfen
if (isValidUser(username, password)) {
// In Sitzung speichern
req.session.userId = user.id;
res.json({ message: 'Anmeldung erfolgreich' });
}
});
// Authentifizierung überprüfen
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Anmeldung erforderlich' });
}
const user = getUserById(req.session.userId);
res.json(user);
});
Vorteile: Einfach, vollständige Serverkontrolle
Nachteile: Erfordert Serverspeicher/Speicherung, geringe Skalierbarkeit
// ========== 2. JWT (JSON Web Token) ==========
// Stateless-Authentifizierung
// Anmeldung
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (isValidUser(username, password)) {
// JWT generieren
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ token });
}
});
// Authentifizierung überprüfen
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET_KEY);
const user = getUserById(decoded.userId);
res.json(user);
} catch (error) {
res.status(401).json({ error: 'Ungültiges Token' });
}
});
Vorteile: Stateless, gute Skalierbarkeit
Nachteile: Schwer zu invalidieren, große Größe
// ========== 3. OAuth 2.0 ==========
// Drittanbieter-Authentifizierung und -Autorisierung
// Benutzer meldet sich mit Google an
// → Google gibt Access Token aus
// → Unsere App ruft Google API mit Token auf
Vorteile:
- Kein Passwort-Sharing
- Granulare Berechtigungskontrolle
- Standardprotokoll
- Social Login möglich
Nachteile:
- Komplex
- Abhängigkeit von externem Dienst
// ========== Vergleich ==========
Merkmal | Session | JWT | OAuth
--------------|---------|----------|-------
Speicherort | Server | Client | Server
Skalierbarkeit| Niedrig | Hoch | Hoch
Sicherheit | Hoch | Mittel | Hoch
Komplexität | Niedrig | Mittel | Hoch
Drittauth | Nein | Nein | Ja
// ========== Kombinierte Verwendung ==========
// JWT nach OAuth-Anmeldung ausgeben
app.get('/auth/google/callback', async (req, res) => {
// 1. Benutzer mit Google OAuth authentifizieren
const googleUser = await getGoogleUser(req.query.code);
// 2. Benutzer in unserer Datenbank speichern
const user = await saveOrUpdateUser(googleUser);
// 3. JWT generieren und zurückgeben
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({ token });
});
// Nachfolgende Anfragen mit JWT authentifizieren
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json(req.user);
});
F2. Was ist der Unterschied zwischen Access Token und Refresh Token?
A:
// ========== Access Token ==========
// Kurze Lebensdauer (15 Minuten~1 Stunde)
// Wird bei API-Anfragen verwendet
// ========== Refresh Token ==========
// Lange Lebensdauer (7 Tage~30 Tage)
// Wird zur Erneuerung des Access Token verwendet
// ========== Implementierungsbeispiel ==========
// Beide bei Anmeldung ausgeben
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
// Access Token (15 Minuten)
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
// Refresh Token (7 Tage)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Refresh Token in DB speichern (zur Invalidierung)
await saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
});
// ========== API-Anfrage (Access Token) ==========
app.get('/api/data', authenticateAccess, (req, res) => {
res.json({ data: 'Wichtige Daten' });
});
function authenticateAccess(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_SECRET);
if (decoded.type !== 'access') {
throw new Error('Falscher Token-Typ');
}
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'access_token_expired',
message: 'Mit Refresh Token erneuern'
});
}
res.status(401).json({ error: 'Ungültiges Token' });
}
}
// ========== Access Token erneuern ==========
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token erforderlich' });
}
try {
// 1. Refresh Token verifizieren
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
if (decoded.type !== 'refresh') {
throw new Error('Falscher Token-Typ');
}
// 2. In DB überprüfen (ob invalidiert)
const isValid = await isRefreshTokenValid(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('Refresh Token invalidiert');
}
// 3. Neues Access Token ausgeben
const newAccessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Ungültiges Refresh Token' });
}
});
// ========== Automatische Erneuerung im Frontend ==========
import axios from 'axios';
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
// Axios-Interceptor
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}
);
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// Access Token abgelaufen
if (error.response?.data?.error === 'access_token_expired' &&
!originalRequest._retry) {
originalRequest._retry = true;
try {
// Mit Refresh Token erneuern
const response = await axios.post('/auth/refresh', {
refreshToken
});
accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);
// Ursprüngliche Anfrage wiederholen
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token auch abgelaufen → Abmelden
localStorage.clear();
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
// ========== Sicherheitstipps ==========
// ✅ Gute Praxis
1. Access Token: Kurze Lebensdauer (15 Minuten)
2. Refresh Token: In DB speichern zur Invalidierung
3. Refresh Token in HttpOnly-Cookie
4. HTTPS obligatorisch
5. Refresh Token Rotation (neues Token bei Erneuerung)
// ❌ Schlechte Praxis
1. Access Token-Lebensdauer zu lang
2. Refresh Token in localStorage speichern
3. Refresh Token nicht invalidierbar
4. HTTP verwenden
F3. Was ist Scope (Berechtigungsumfang)?
R:
// ========== Scope-Konzept ==========
// Umfang der Berechtigungen, die die Anwendung anfordert
// ========== Google OAuth Scopes ==========
const scopes = [
'openid', // Basisinformationen
'email', // E-Mail
'profile', // Profil (Name, Foto)
'https://www.googleapis.com/auth/drive.readonly', // Drive-Leserechte
'https://www.googleapis.com/auth/gmail.send' // Gmail-Versand
];
// Berechtigungen bei Anmeldung anfordern
app.get('/auth/google', (req, res) => {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: scopes.join(' ') // Mit Leerzeichen trennen
});
res.redirect(`${authUrl}?${params}`);
});
// Dem Benutzer angezeigter Bildschirm:
/*
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
My App fordert an:
✓ E-Mail-Adresse ansehen
✓ Grundlegende Profilinformationen ansehen
✓ Google Drive-Dateien ansehen
✓ Gmail-E-Mails senden
[Zulassen] [Verweigern]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
*/
// ========== GitHub OAuth Scopes ==========
const githubScopes = [
'user', // Benutzerinformationen
'user:email', // E-Mail (inkl. privat)
'repo', // Vollständiger Repository-Zugriff
'public_repo', // Nur öffentliche Repositories
'read:org', // Organisationsinformationen lesen
'write:org' // Organisationsinformationen schreiben
];
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email public_repo' // Nur benötigte Berechtigungen
});
res.redirect(`${authUrl}?${params}`);
});
// ========== Prinzip der minimalen Berechtigung ==========
// ❌ Schlechte Praxis: Mehr Berechtigungen als nötig anfordern
scope: 'user repo admin:org' // Zu viele Berechtigungen!
// ✅ Gute Praxis: Nur benötigte Berechtigungen
scope: 'user:email' // Nur E-Mail benötigt
// ========== Dynamische Scope-Anforderung ==========
// Erste Anmeldung: Nur Basisinformationen
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile' // Nur Basisinformationen
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// Wenn später zusätzliche Berechtigungen benötigt werden
app.get('/auth/google/drive', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/drive.readonly',
prompt: 'consent' // Erneut Zustimmung anfordern
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// ========== Inkrementelle Autorisierung ==========
// Berechtigungen schrittweise hinzufügen, wenn benötigt
// Schritt 1: Anmeldung (Basisinformationen)
scope: 'email profile'
// Schritt 2: Google Drive-Integration (wenn Benutzer möchte)
scope: 'https://www.googleapis.com/auth/drive'
// Schritt 3: Gmail-Integration (wenn Benutzer möchte)
scope: 'https://www.googleapis.com/auth/gmail.send'
// ========== Scope-Verifizierung ==========
// Im Access Token enthaltenen Scope überprüfen
async function checkScope(accessToken, requiredScope) {
const response = await axios.get(
'https://www.googleapis.com/oauth2/v1/tokeninfo',
{ params: { access_token: accessToken } }
);
const grantedScopes = response.data.scope.split(' ');
if (!grantedScopes.includes(requiredScope)) {
throw new Error('Unzureichende Berechtigungen');
}
}
// Verwendungsbeispiel
app.get('/api/drive/files', async (req, res) => {
const accessToken = req.headers.authorization?.split(' ')[1];
try {
// Drive-Berechtigung überprüfen
await checkScope(accessToken, 'https://www.googleapis.com/auth/drive.readonly');
// Drive-API aufrufen
const files = await listDriveFiles(accessToken);
res.json(files);
} catch (error) {
res.status(403).json({
error: 'Unzureichende Berechtigungen',
message: 'Google Drive-Zugriff erforderlich'
});
}
});
F4. Was sind OAuth-Sicherheits-Best-Practices?
A:
// ========== 1. HTTPS obligatorisch ==========
// ✅ Immer HTTPS verwenden
const REDIRECT_URI = 'https://myapp.com/callback';
// ❌ HTTP absolut verboten
const REDIRECT_URI = 'http://myapp.com/callback'; // Token-Diebstahl-Risiko!
// ========== 2. State-Parameter (CSRF-Schutz) ==========
// Anmeldung starten
app.get('/auth/google', (req, res) => {
// Zufälligen State generieren und speichern
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
state // CSRF-Schutz
});
res.redirect(`${authUrl}?${params}`);
});
// Im Callback verifizieren
app.get('/auth/google/callback', (req, res) => {
const { state, code } = req.query;
// State überprüfen
if (state !== req.session.oauthState) {
return res.status(403).send('CSRF-Angriff erkannt');
}
// State löschen
delete req.session.oauthState;
// ... Weiterverarbeitung
});
// ========== 3. Client Secret schützen ==========
// ✅ Umgebungsvariablen verwenden
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
// ✅ Nur auf Server verwenden
// Niemals im Frontend offenlegen!
// ❌ Nicht im Code hardcodieren
const CLIENT_SECRET = 'abc123xyz'; // Absolut verboten!
// ========== 4. Redirect URI validieren ==========
// Nur in Google Cloud Console registrierte URIs erlauben
// ✅ Exakt übereinstimmende URI
https://myapp.com/auth/callback
// ❌ Keine Wildcards verwenden
https://myapp.com/*
// ❌ Offene Weiterleitungsschwachstelle
https://myapp.com/callback?redirect=evil.com
// ========== 5. Token-Speicherung ==========
// ✅ Server: Umgebungsvariablen oder verschlüsselte DB
await saveToken(userId, encryptToken(accessToken));
// ✅ Client: HttpOnly-Cookie (XSS-Schutz)
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // Nur HTTPS
sameSite: 'strict',
maxAge: 3600000 // 1 Stunde
});
// ❌ localStorage (XSS-anfällig)
localStorage.setItem('token', accessToken); // Gefährlich!
// ========== 6. Token-Ablaufzeit ==========
// ✅ Kurze Lebensdauer
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15 Minuten
});
// ✅ Refresh Token verwenden
const refreshToken = jwt.sign(payload, secret, {
expiresIn: '7d' // 7 Tage
});
// ❌ Keine Ablaufzeit
const token = jwt.sign(payload, secret); // Gefährlich!
// ========== 7. Scope minimieren ==========
// ✅ Nur benötigte Berechtigungen
scope: 'email profile'
// ❌ Alle Berechtigungen anfordern
scope: 'https://www.googleapis.com/auth/drive' // Unnötig
// ========== 8. Token-Widerruf implementieren ==========
// Token beim Abmelden widerrufen
app.get('/logout', async (req, res) => {
const accessToken = req.cookies.access_token;
// Google Token widerrufen
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
// Refresh Token aus DB löschen
await deleteRefreshToken(req.user.id);
// Cookie löschen
res.clearCookie('access_token');
res.redirect('/');
});
// ========== 9. PKCE verwenden (Mobil/SPA) ==========
// Sicherheit ohne Client Secret verbessern
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));
// Challenge in Authentifizierungsanfrage senden
const params = {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};
// Verifier beim Token-Austausch senden
const tokenParams = {
code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // Statt Secret
};
// ========== 10. Regelmäßige Token-Erneuerung ==========
// Automatische Erneuerung im Hintergrund
setInterval(async () => {
try {
const newAccessToken = await refreshAccessToken(refreshToken);
updateToken(newAccessToken);
} catch (error) {
console.error('Token-Erneuerung fehlgeschlagen:', error);
redirectToLogin();
}
}, 14 * 60 * 1000); // Alle 14 Minuten (vor Ablauf nach 15 Min.)
F5. Authentifizierung (Authentication) vs Autorisierung (Authorization)?
A:
// ========== Authentifizierung (Authentication) ==========
// "Wer bist du?" - Identitätsverifizierung
// Beispiel: Anmeldung
const user = await authenticateUser(username, password);
if (user) {
console.log('Du bist Max Müller ✓');
}
// ========== Autorisierung (Authorization) ==========
// "Was kannst du tun?" - Berechtigungsverifizierung
// Beispiel: Nur Administrator-Zugriff
if (user.role === 'admin') {
console.log('Administrator-Funktionen verfügbar ✓');
} else {
console.log('Keine Berechtigung ✗');
}
// ========== Rolle von OAuth 2.0 ==========
// OAuth ist hauptsächlich Authorization (Autorisierung)
// - Benutzer erteilt App Berechtigung
// - "Diese App darf mein Google Drive sehen"
// Wird aber auch für Authentifizierung (Authentication) verwendet
// - OpenID Connect (OAuth 2.0 + Authentifizierung)
// - "Mit Google anmelden"
// ========== Echtes Beispiel ==========
// Schritt 1: Authentifizierung (Authentication)
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Benutzeridentität verifizieren
const user = await User.findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Authentifizierung fehlgeschlagen' });
}
// Authentifizierung erfolgreich → Token ausgeben
const token = jwt.sign(
{
userId: user.id,
role: user.role // Berechtigungsinformationen einbeziehen
},
SECRET_KEY
);
res.json({ token });
});
// Schritt 2: Autorisierung (Authorization)
app.get('/admin/users', authenticateToken, authorizeAdmin, (req, res) => {
// Nur Administratoren können zugreifen
const users = await User.findAll();
res.json(users);
});
function authenticateToken(req, res, next) {
// Authentifizierung: Token verifizieren
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Authentifizierung erforderlich' });
}
}
function authorizeAdmin(req, res, next) {
// Autorisierung: Administrator überprüfen
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Keine Berechtigung' });
}
next();
}
// ========== Authentifizierung + Autorisierung mit OAuth ==========
// 1. Authentifizierung (OpenID Connect)
app.get('/auth/google/callback', async (req, res) => {
// Benutzer mit Google OAuth verifizieren
const googleUser = await getGoogleUser(req.query.code);
// Authentifizierung abgeschlossen → Identität verifiziert
console.log('Benutzer:', googleUser.email);
// In unserem System anmelden
const user = await findOrCreateUser(googleUser);
req.session.userId = user.id;
});
// 2. Autorisierung (Scope)
app.get('/api/drive/files', async (req, res) => {
// Google Drive-Berechtigung überprüfen
const hasPermission = await checkGoogleScope(
req.user.accessToken,
'https://www.googleapis.com/auth/drive.readonly'
);
if (!hasPermission) {
return res.status(403).json({
error: 'Berechtigung erforderlich',
message: 'Bitte Google Drive-Zugriff gewähren'
});
}
// Hat Berechtigung → API aufrufen
const files = await listDriveFiles(req.user.accessToken);
res.json(files);
});
// ========== Vergleichszusammenfassung ==========
Authentifizierung (Authentication)
- Frage: "Wer bist du?"
- Zweck: Identität verifizieren
- Beispiele:
* Passwort eingeben
* Fingerabdruck-Erkennung
* Mit Google anmelden
- HTTP-Statuscode: 401 Unauthorized
Autorisierung (Authorization)
- Frage: "Was kannst du tun?"
- Zweck: Zugriffsberechtigungen verifizieren
- Beispiele:
* Administrator-Rechte
* Google Drive-Leseberechtigung
* OAuth Scope
- HTTP-Statuscode: 403 Forbidden
// Beides wird benötigt!
// 1. Zuerst Authentifizierung (Anmeldung)
// 2. Dann Berechtigungen verifizieren (Zugriff gewähren)
🎓 Nächste Schritte
Wenn Sie OAuth 2.0 verstanden haben, versuchen Sie Folgendes zu lernen:
- JWT Token (Dokument in Vorbereitung) - Token-basierte Authentifizierung
- Was ist HTTPS? (Dokument in Vorbereitung) - Sichere Kommunikation
- Was ist CORS? (Dokument in Vorbereitung) - API-Sicherheit
Praxis
# ========== 1. Google OAuth-Praxis ==========
# Projekt erstellen
mkdir oauth-demo
cd oauth-demo
npm init -y
# Pakete installieren
npm install express axios express-session dotenv
# .env-Datei erstellen
cat > .env << EOF
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
SESSION_SECRET=your_session_secret
EOF
# server.js schreiben und ausführen
node server.js
# http://localhost:3000 aufrufen
# ========== 2. Passport.js-Praxis ==========
npm install passport passport-google-oauth20
# ========== 3. Mehrere Social Logins implementieren ==========
npm install passport-github2 passport-facebook
# Google, GitHub, Facebook unterstützen
🎬 Fazit
OAuth 2.0 ist das Standard-Authentifizierungsprotokoll des modernen Webs:
- Sicherheit: Sichere Authentifizierung ohne Passwort-Sharing
- Bequemlichkeit: Schnelle Registrierung mit Social Login
- Berechtigungskontrolle: Nur benötigte Berechtigungen erteilen
- Standard: Von allen wichtigen Diensten unterstützt
Bauen Sie ein sicheres und bequemes Authentifizierungssystem auf! 🔐