@gftdcojp/ksqldb-orm
Version:
ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions
566 lines • 19.7 kB
JavaScript
"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