Refresh-Token-System: kurzlebiger Access-Token (15m) + rotierender Refresh-Token (90d)
All checks were successful
Deploy GSM / deploy (push) Successful in 29s
All checks were successful
Deploy GSM / deploy (push) Successful in 29s
Bisher: 7d JWT im localStorage. Jeder 403 loggte den User aus — auch "Insufficient permissions"-Responses fuer Background-Requests — wodurch User sich effektiv taeglich neu einloggen mussten. Backend: - Neue refresh_tokens Tabelle (SHA256-gehasht, mit Rotation + Revoke) - /auth/refresh tauscht Refresh-Token gegen frischen Access-Token (rotiert beide) - /auth/logout revoked den Refresh-Token - Access-Token-TTL auf 15m; Guest-Refresh 7d, Discord-Refresh 90d - Daily cleanup-Job fuer abgelaufene/revoked Tokens Frontend: - api.js refreshed bei 401 automatisch (single-flight) und retryt die Anfrage - 403 wird nicht mehr als Session-Ablauf behandelt - Discord-Callback + Guest-Login liefern beide Tokens - App.jsx synced React-State per gsm_token_refreshed CustomEvent Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
gsm-backend/services/refreshTokens.js
Normal file
61
gsm-backend/services/refreshTokens.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import crypto from 'crypto';
|
||||
import { db } from '../db/init.js';
|
||||
|
||||
// Refresh token lifetime
|
||||
const DISCORD_REFRESH_TTL_DAYS = 90;
|
||||
const GUEST_REFRESH_TTL_DAYS = 7;
|
||||
|
||||
function hashToken(token) {
|
||||
return crypto.createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
|
||||
function generateToken() {
|
||||
return crypto.randomBytes(48).toString('base64url');
|
||||
}
|
||||
|
||||
function ttlDaysFor(userType) {
|
||||
return userType === 'guest' ? GUEST_REFRESH_TTL_DAYS : DISCORD_REFRESH_TTL_DAYS;
|
||||
}
|
||||
|
||||
export function createRefreshToken({ userType, discordUserId = null, guestId = null, guestUsername = null }) {
|
||||
const token = generateToken();
|
||||
const tokenHash = hashToken(token);
|
||||
const expiresAt = new Date(Date.now() + ttlDaysFor(userType) * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO refresh_tokens (token_hash, user_type, discord_user_id, guest_id, guest_username, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(tokenHash, userType, discordUserId, guestId, guestUsername, expiresAt);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export function verifyRefreshToken(token) {
|
||||
if (!token) return null;
|
||||
const tokenHash = hashToken(token);
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM refresh_tokens
|
||||
WHERE token_hash = ?
|
||||
AND revoked = 0
|
||||
AND expires_at > CURRENT_TIMESTAMP
|
||||
`).get(tokenHash);
|
||||
return row || null;
|
||||
}
|
||||
|
||||
export function revokeRefreshToken(token) {
|
||||
if (!token) return;
|
||||
const tokenHash = hashToken(token);
|
||||
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?').run(tokenHash);
|
||||
}
|
||||
|
||||
export function revokeRefreshTokenById(id) {
|
||||
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
export function touchRefreshToken(id) {
|
||||
db.prepare('UPDATE refresh_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
export function revokeAllForDiscordUser(discordUserId) {
|
||||
db.prepare('UPDATE refresh_tokens SET revoked = 1 WHERE discord_user_id = ?').run(discordUserId);
|
||||
}
|
||||
Reference in New Issue
Block a user