UNPKG

@gftdcojp/ksqldb-orm

Version:

ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions

566 lines 19.7 kB
"use strict"; /** * JWT認証システム - Supabase風のJWT認証実装 */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.jwtAuth = exports.JwtAuthManager = void 0; exports.jwtAuthMiddleware = jwtAuthMiddleware; const jwt = __importStar(require("jsonwebtoken")); const uuid_1 = require("uuid"); const logger_1 = require("./utils/logger"); /** * メモリベースのリフレッシュトークンストレージ(開発用のみ) */ class MemoryRefreshTokenStorage { constructor() { this.tokens = new Map(); } async store(tokenId, data) { this.tokens.set(tokenId, { ...data, createdAt: Date.now(), lastUsedAt: Date.now() }); logger_1.log.warn('WARNING: Using memory-based refresh token storage. Tokens will be lost on server restart.'); } async get(tokenId) { const data = this.tokens.get(tokenId); if (data) { // 使用時刻を更新 data.lastUsedAt = Date.now(); this.tokens.set(tokenId, data); } return data || null; } async delete(tokenId) { this.tokens.delete(tokenId); } async cleanup() { const now = Date.now(); let deletedCount = 0; for (const [tokenId, data] of this.tokens.entries()) { if (now > data.expiresAt) { this.tokens.delete(tokenId); deletedCount++; } } return deletedCount; } async deleteAllForUser(userId) { for (const [tokenId, data] of this.tokens.entries()) { if (data.userId === userId) { this.tokens.delete(tokenId); } } } } /** * Redis対応のリフレッシュトークンストレージ */ class RedisRefreshTokenStorage { constructor(redis, keyPrefix = 'refresh_token:') { this.redis = redis; this.keyPrefix = keyPrefix; } async store(tokenId, data) { const key = this.keyPrefix + tokenId; const tokenData = { ...data, createdAt: Date.now(), lastUsedAt: Date.now() }; const ttl = Math.ceil((data.expiresAt - Date.now()) / 1000); await this.redis.setex(key, ttl, JSON.stringify(tokenData)); } async get(tokenId) { const key = this.keyPrefix + tokenId; const dataStr = await this.redis.get(key); if (!dataStr) { return null; } const data = JSON.parse(dataStr); // 使用時刻を更新 data.lastUsedAt = Date.now(); const ttl = await this.redis.ttl(key); if (ttl > 0) { await this.redis.setex(key, ttl, JSON.stringify(data)); } return data; } async delete(tokenId) { const key = this.keyPrefix + tokenId; await this.redis.del(key); } async cleanup() { // Redis では TTL により自動的にクリーンアップされるため、 // 期限切れキーの数を正確に返すのは困難 // ここでは手動でのクリーンアップは不要 return 0; } async deleteAllForUser(userId) { const pattern = this.keyPrefix + '*'; const keys = await this.redis.keys(pattern); for (const key of keys) { const dataStr = await this.redis.get(key); if (dataStr) { const data = JSON.parse(dataStr); if (data.userId === userId) { await this.redis.del(key); } } } } } /** * PostgreSQL対応のリフレッシュトークンストレージ */ class PostgreSQLRefreshTokenStorage { constructor(db, tableName = 'refresh_tokens') { this.db = db; this.tableName = tableName; } async store(tokenId, data) { const now = new Date(); const expiresAt = new Date(data.expiresAt); await this.db.query(` INSERT INTO ${this.tableName} (token_id, user_id, expires_at, created_at, last_used_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (token_id) DO UPDATE SET user_id = EXCLUDED.user_id, expires_at = EXCLUDED.expires_at, last_used_at = EXCLUDED.last_used_at `, [tokenId, data.userId, expiresAt, now, now]); } async get(tokenId) { const result = await this.db.query(` SELECT user_id, expires_at, created_at, last_used_at FROM ${this.tableName} WHERE token_id = $1 AND expires_at > NOW() `, [tokenId]); if (result.rows.length === 0) { return null; } const row = result.rows[0]; // 使用時刻を更新 await this.db.query(` UPDATE ${this.tableName} SET last_used_at = NOW() WHERE token_id = $1 `, [tokenId]); return { userId: row.user_id, expiresAt: new Date(row.expires_at).getTime(), createdAt: new Date(row.created_at).getTime(), lastUsedAt: Date.now() }; } async delete(tokenId) { await this.db.query(` DELETE FROM ${this.tableName} WHERE token_id = $1 `, [tokenId]); } async cleanup() { const result = await this.db.query(` DELETE FROM ${this.tableName} WHERE expires_at <= NOW() `); return result.rowCount || 0; } async deleteAllForUser(userId) { await this.db.query(` DELETE FROM ${this.tableName} WHERE user_id = $1 `, [userId]); } } /** * JWT認証管理クラス */ class JwtAuthManager { constructor() { // 環境変数の安全な取得(ブラウザ環境では undefined) const getEnvVar = (key) => { try { return typeof process !== 'undefined' && process.env ? process.env[key] : undefined; } catch { return undefined; } }; // セキュリティ: JWT秘密鍵は必須で環境変数から取得 const jwtSecret = getEnvVar('GFTD_JWT_SECRET'); if (!jwtSecret) { throw new Error('CRITICAL SECURITY ERROR: GFTD_JWT_SECRET environment variable is required. Please set a cryptographically secure 64+ character secret key.'); } // セキュリティ: 秘密鍵の最小長チェック if (jwtSecret.length < 32) { throw new Error('CRITICAL SECURITY ERROR: GFTD_JWT_SECRET must be at least 32 characters long for security. Generate with: openssl rand -hex 32'); } this.config = { secret: jwtSecret, expiresIn: getEnvVar('GFTD_JWT_EXPIRES_IN') || '15m', refreshExpiresIn: getEnvVar('GFTD_JWT_REFRESH_EXPIRES_IN') || '7d', issuer: getEnvVar('GFTD_JWT_ISSUER') || 'gftd-orm', audience: getEnvVar('GFTD_JWT_AUDIENCE') || 'gftd-orm-client', }; // デフォルトではメモリストレージを使用(開発用) this.refreshTokenStorage = new MemoryRefreshTokenStorage(); // 本番環境では永続ストレージの使用を警告 if (getEnvVar('NODE_ENV') === 'production') { logger_1.log.error('CRITICAL SECURITY WARNING: Using memory-based refresh token storage in production. Please configure Redis or PostgreSQL for refresh token persistence.'); } } /** * シングルトンインスタンスを取得 */ static getInstance() { if (!JwtAuthManager.instance) { JwtAuthManager.instance = new JwtAuthManager(); } return JwtAuthManager.instance; } /** * リフレッシュトークンストレージを設定 * @param storage カスタムストレージ実装 */ setRefreshTokenStorage(storage) { this.refreshTokenStorage = storage; } /** * Redisストレージを設定 * @param redis Redisクライアント * @param keyPrefix キーのプレフィックス */ setRedisStorage(redis, keyPrefix) { this.refreshTokenStorage = new RedisRefreshTokenStorage(redis, keyPrefix); logger_1.log.info('Redis refresh token storage configured'); } /** * PostgreSQLストレージを設定 * @param db PostgreSQLクライアント * @param tableName テーブル名 */ setPostgreSQLStorage(db, tableName) { this.refreshTokenStorage = new PostgreSQLRefreshTokenStorage(db, tableName); logger_1.log.info('PostgreSQL refresh token storage configured'); } /** * アクセストークンを生成 */ generateAccessToken(user) { const payload = { sub: user.sub, email: user.email, role: user.role, tenant_id: user.tenant_id, metadata: user.metadata, app_metadata: user.app_metadata, user_metadata: user.user_metadata, aud: this.config.audience, iss: this.config.issuer, iat: Math.floor(Date.now() / 1000), }; // セキュリティ: アルゴリズムを明示的に指定してnoneアルゴリズム攻撃を防止 return jwt.sign(payload, this.config.secret, { expiresIn: this.config.expiresIn, algorithm: 'HS256' }); } /** * リフレッシュトークンを生成 */ async generateRefreshToken(userId) { const tokenId = (0, uuid_1.v4)(); const expiresAt = Date.now() + this.parseExpiration(this.config.refreshExpiresIn); // リフレッシュトークンを永続ストレージに保存 await this.refreshTokenStorage.store(tokenId, { userId, expiresAt, }); return tokenId; } /** * 認証トークンのペアを生成 */ async generateAuthTokens(user) { const accessToken = this.generateAccessToken(user); const refreshToken = await this.generateRefreshToken(user.sub); const decoded = jwt.decode(accessToken); const expiresAt = decoded.exp * 1000; // ミリ秒に変換 const expiresIn = Math.floor((expiresAt - Date.now()) / 1000); logger_1.log.info(`Generated auth tokens for user`, { userId: user.sub }); return { accessToken, refreshToken, user, expiresAt, expiresIn, }; } /** * アクセストークンを検証 */ verifyAccessToken(token) { try { // セキュリティ: アルゴリズムを明示的に指定して検証 const decoded = jwt.verify(token, this.config.secret, { algorithms: ['HS256'], issuer: this.config.issuer, audience: this.config.audience }); return { sub: decoded.sub, email: decoded.email, role: decoded.role, tenant_id: decoded.tenant_id, metadata: decoded.metadata, app_metadata: decoded.app_metadata, user_metadata: decoded.user_metadata, }; } catch (error) { logger_1.log.warn(`Invalid access token`, { error: error instanceof Error ? error.message : 'Unknown error' }); return null; } } /** * リフレッシュトークンを検証 */ async verifyRefreshToken(refreshToken) { const tokenData = await this.refreshTokenStorage.get(refreshToken); if (!tokenData) { logger_1.log.warn(`Invalid refresh token`, { token: '[REDACTED]' }); return null; } if (Date.now() > tokenData.expiresAt) { logger_1.log.warn(`Expired refresh token`, { token: '[REDACTED]' }); await this.refreshTokenStorage.delete(refreshToken); return null; } return tokenData.userId; } /** * リフレッシュトークンを使用してアクセストークンを更新 */ async refreshAccessToken(refreshToken, currentUser) { const userId = await this.verifyRefreshToken(refreshToken); if (!userId || userId !== currentUser.sub) { logger_1.log.warn('Invalid refresh token', { userId: currentUser.sub }); return null; } // 新しいトークンペアを生成 const authResult = await this.generateAuthTokens(currentUser); // 古いリフレッシュトークンを削除 await this.refreshTokenStorage.delete(refreshToken); logger_1.log.info(`Refreshed access token for user`, { userId: currentUser.sub }); return authResult; } /** * 匿名ユーザーのトークンを生成 */ async generateAnonymousToken(tenantId) { const anonUser = { sub: `anon-${(0, uuid_1.v4)()}`, role: 'anon', tenant_id: tenantId || 'default', metadata: {}, app_metadata: { provider: 'anonymous' }, user_metadata: {}, }; return this.generateAuthTokens(anonUser); } /** * サービスロールトークンを生成 */ async generateServiceRoleToken(tenantId) { const serviceUser = { sub: `service-${(0, uuid_1.v4)()}`, role: 'service_role', tenant_id: tenantId || 'default', metadata: {}, app_metadata: { provider: 'service' }, user_metadata: {}, }; return this.generateAuthTokens(serviceUser); } /** * トークンを無効化 */ async revokeToken(refreshToken) { await this.refreshTokenStorage.delete(refreshToken); logger_1.log.info(`Revoked refresh token`, { token: '[REDACTED]' }); } /** * ユーザーの全トークンを無効化 */ async revokeAllTokensForUser(userId) { await this.refreshTokenStorage.deleteAllForUser(userId); logger_1.log.info(`Revoked all refresh tokens for user`, { userId }); } /** * 期限切れのリフレッシュトークンをクリーンアップ */ async cleanupExpiredTokens() { const cleanedCount = await this.refreshTokenStorage.cleanup(); if (cleanedCount > 0) { logger_1.log.info(`Cleaned up expired refresh tokens`, { count: cleanedCount }); } } /** * 有効期限の文字列をミリ秒に変換 */ parseExpiration(expiration) { const units = { 's': 1000, 'm': 60 * 1000, 'h': 60 * 60 * 1000, 'd': 24 * 60 * 60 * 1000, 'w': 7 * 24 * 60 * 60 * 1000, }; const match = expiration.match(/^(\d+)([smhdw])$/); if (!match) { throw new Error(`Invalid expiration format: ${expiration}`); } const value = parseInt(match[1]); const unit = match[2]; return value * units[unit]; } } exports.JwtAuthManager = JwtAuthManager; /** * Express.js ミドルウェア: JWT認証 */ function jwtAuthMiddleware(options = {}) { const authManager = JwtAuthManager.getInstance(); const { requireAuth = true, allowAnonymous = false, requiredRole } = options; return (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { if (!requireAuth || allowAnonymous) { // 匿名ユーザーとして処理 req.user = { sub: `anon-${(0, uuid_1.v4)()}`, role: 'anon', tenant_id: 'default', }; return next(); } else { return res.status(401).json({ error: 'Unauthorized', message: 'Missing authorization header', }); } } const token = authHeader.replace('Bearer ', ''); const user = authManager.verifyAccessToken(token); if (!user) { logger_1.log.warn('Invalid access token', { ip: req.ip }); return res.status(401).json({ error: 'Unauthorized', message: 'Invalid access token', }); } // ロールチェック if (requiredRole && user.role !== requiredRole) { logger_1.log.warn('Insufficient role', { userId: user.sub, tenantId: user.tenant_id, required: requiredRole, actual: user.role }); return res.status(403).json({ error: 'Forbidden', message: 'Insufficient role', }); } req.user = user; next(); }; } /** * JWT認証のヘルパー関数 */ exports.jwtAuth = { /** * 認証マネージャーのインスタンスを取得 */ manager: () => JwtAuthManager.getInstance(), /** * ユーザーを認証してトークンを発行 */ authenticate: (user) => { const authManager = JwtAuthManager.getInstance(); return authManager.generateAuthTokens(user); }, /** * 匿名認証トークンを発行 */ authenticateAnonymous: (tenantId) => { const authManager = JwtAuthManager.getInstance(); return authManager.generateAnonymousToken(tenantId); }, /** * サービスロール認証トークンを発行 */ authenticateServiceRole: (tenantId) => { const authManager = JwtAuthManager.getInstance(); return authManager.generateServiceRoleToken(tenantId); }, /** * トークンを検証 */ verify: (token) => { const authManager = JwtAuthManager.getInstance(); return authManager.verifyAccessToken(token); }, /** * トークンをリフレッシュ */ refresh: (refreshToken, currentUser) => { const authManager = JwtAuthManager.getInstance(); return authManager.refreshAccessToken(refreshToken, currentUser); }, /** * トークンを無効化 */ revoke: (refreshToken) => { const authManager = JwtAuthManager.getInstance(); authManager.revokeToken(refreshToken); }, }; //# sourceMappingURL=jwt-auth.js.map