UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

406 lines 15.3 kB
/** * 統一認証設定クラス - auth.gftd.ai カスタムドメイン専用 * * 全てのGFTDサービスでauth.gftd.aiドメインでの統一認証を実現 * 各サービスではローカル認証コンポーネントを持たず、auth.gftd.aiにリダイレクト */ import { log } from './utils/logger'; /** * 統一認証マネージャー * * auth.gftd.aiドメインでの統一認証フローを管理 */ export class UnifiedAuthManager { constructor(config) { // 🔐 *.gftd.ai 共通ログイン: サービスURLから自動検出 const isGftdDomain = this.isGftdDomainService(config?.service?.baseUrl); const sharedSession = this.getSharedSessionConfig(config?.session?.shared, isGftdDomain); // ClientIDの値を決定(明示的に空文字列が渡された場合はそのまま使用) let clientId; if (config?.oauth?.clientId !== undefined) { // 明示的にclientIdが設定された場合(空文字列を含む)はそのまま使用 clientId = config.oauth.clientId; } else { // 設定されていない場合は環境変数またはデフォルト値を使用 clientId = process.env.GFTD_AUTH0_CLIENT_ID || process.env.AUTH0_CLIENT_ID || 'k0ziPQ6IkDxE1AUSvzx5PwXtnf4y81x0'; } this.config = { authDomain: 'auth.gftd.ai', oauth: { clientId, clientSecret: process.env.GFTD_AUTH0_CLIENT_SECRET || process.env.AUTH0_CLIENT_SECRET || config?.oauth?.clientSecret, audience: process.env.GFTD_AUTH0_AUDIENCE || process.env.AUTH0_AUDIENCE || `https://auth.gftd.ai/api/v2/`, scope: config?.oauth?.scope || 'openid profile email', }, redirects: { defaultPostLogin: config?.redirects?.defaultPostLogin || '/', defaultPostLogout: config?.redirects?.defaultPostLogout || '/', callbackPath: config?.redirects?.callbackPath || '/auth/callback', }, session: { secretKey: this.getServiceSecretKey(config?.service?.name, config?.session?.secretKey), maxAge: config?.session?.maxAge || 7 * 24 * 60 * 60, // 7日 rolling: config?.session?.rolling ?? true, cookie: { name: sharedSession.enabled ? sharedSession.cookieName : (config?.session?.cookie?.name || this.getServiceCookieName(config?.service?.name)), secure: process.env.NODE_ENV === 'production', sameSite: config?.session?.cookie?.sameSite || 'lax', domain: sharedSession.enabled ? sharedSession.cookieDomain : config?.session?.cookie?.domain, path: config?.session?.cookie?.path || '/', ...config?.session?.cookie, }, shared: sharedSession, ...config?.session, }, service: { name: config?.service?.name || 'GFTD Service', baseUrl: config?.service?.baseUrl || process.env.GFTD_URL || 'http://localhost:3000', additionalScopes: config?.service?.additionalScopes || [], }, }; this.validateConfig(); log.info(`Unified Auth Manager initialized for service: ${this.config.service.name}`); log.info(`Auth domain: ${this.config.authDomain}`); } /** * シングルトンインスタンスを取得 */ static getInstance(config) { if (!UnifiedAuthManager.instance || config) { UnifiedAuthManager.instance = new UnifiedAuthManager(config); } return UnifiedAuthManager.instance; } /** * サービス固有のセッション暗号化キーを取得 * * 優先順位: * 1. 直接指定されたsecretKey * 2. サービス名ベースの環境変数 (GFTD_{SERVICE}_SESSION_SECRET) * 3. 統一環境変数 (GFTD_SESSION_SECRET, AUTH0_SECRET) */ getServiceSecretKey(serviceName, explicitKey) { if (explicitKey) { return explicitKey; } // サービス名ベースの環境変数を試行 if (serviceName) { // 'GFTD Webmaster' -> 'WEBMASTER', 'GFTD CLI' -> 'CLI' const serviceKey = serviceName .replace(/^GFTD\s+/i, '') // 先頭の 'GFTD ' を除去 .toUpperCase() .replace(/[^A-Z0-9]/g, '_'); const envVarName = `GFTD_${serviceKey}_SESSION_SECRET`; const serviceEnvKey = process.env[envVarName]; if (serviceEnvKey) { return serviceEnvKey; } } // フォールバック: 統一環境変数 return process.env.GFTD_SESSION_SECRET || process.env.AUTH0_SECRET || ''; } /** * サービス固有のCookie名を取得 */ getServiceCookieName(serviceName) { if (serviceName) { // 'GFTD Webmaster' -> 'webmaster', 'GFTD CLI' -> 'cli' const serviceKey = serviceName .replace(/^GFTD\s+/i, '') // 先頭の 'GFTD ' を除去 .toLowerCase() .replace(/[^a-z0-9]/g, '-'); return `gftd-${serviceKey}-session`; } return 'gftd-auth-session'; } /** * サービスが *.gftd.ai ドメインかを判定 */ isGftdDomainService(baseUrl) { if (!baseUrl) { return false; } try { const url = new URL(baseUrl); const hostname = url.hostname.toLowerCase(); // *.gftd.ai ドメインパターンをチェック return hostname === 'gftd.ai' || hostname.endsWith('.gftd.ai'); } catch (error) { return false; } } /** * 共通セッション設定を取得 */ getSharedSessionConfig(sharedConfig, autoDetectGftdDomain = false) { // 明示的に無効化されている場合 if (sharedConfig?.enabled === false) { return { enabled: false, cookieDomain: '', cookieName: '', }; } // 明示的に有効化されている場合、または自動検出で *.gftd.ai の場合 const shouldEnable = sharedConfig?.enabled === true || (sharedConfig?.enabled !== false && autoDetectGftdDomain); if (shouldEnable) { return { enabled: true, cookieDomain: sharedConfig?.cookieDomain || '.gftd.ai', cookieName: sharedConfig?.cookieName || 'gftd-shared-session', }; } // デフォルト: 無効 return { enabled: false, cookieDomain: '', cookieName: '', }; } /** * 設定の検証 */ validateConfig() { const { oauth, session } = this.config; if (!oauth.clientId || oauth.clientId.trim() === '') { throw new Error('OAuth Client ID is required for unified authentication'); } if (!session.secretKey) { throw new Error('Session secret key is required for unified authentication'); } if (session.secretKey.length < 32) { throw new Error('Session secret key must be at least 32 characters'); } } /** * 統一ログインURLを生成 * * @param options ログインオプション * @returns auth.gftd.aiのログインURL */ buildUnifiedLoginUrl(options = {}) { const { returnTo = this.config.redirects.defaultPostLogin, state, connection, prompt, } = options; // リダイレクトURIを構築 const redirectUri = `${this.config.service.baseUrl}${this.config.redirects.callbackPath}`; // スコープを構築(サービス固有スコープを追加) const allScopes = [ this.config.oauth.scope, ...(this.config.service.additionalScopes || []), ].join(' '); // 状態情報を構築 const stateData = { returnTo, service: this.config.service.name, timestamp: Date.now(), ...(state ? { customState: state } : {}), }; const encodedState = Buffer.from(JSON.stringify(stateData)).toString('base64'); // URLパラメータを構築 const params = new URLSearchParams({ client_id: this.config.oauth.clientId, response_type: 'code', redirect_uri: redirectUri, scope: allScopes, state: encodedState, audience: this.config.oauth.audience, }); if (connection) params.append('connection', connection); if (prompt) params.append('prompt', prompt); const loginUrl = `https://${this.config.authDomain}/authorize?${params.toString()}`; log.info(`Generated unified login URL for service: ${this.config.service.name}`); log.debug(`Login URL: ${loginUrl}`); return loginUrl; } /** * 統一ログアウトURLを生成 * * @param options ログアウトオプション * @returns auth.gftd.aiのログアウトURL */ buildUnifiedLogoutUrl(options = {}) { const { returnTo = this.config.redirects.defaultPostLogout, federated = false, clearSharedSession = this.config.session.shared?.enabled, } = options; // 完全なreturnTo URLを構築 const fullReturnTo = returnTo.startsWith('http') ? returnTo : `${this.config.service.baseUrl}${returnTo}`; const params = new URLSearchParams({ returnTo: fullReturnTo, client_id: this.config.oauth.clientId, }); if (federated) { params.append('federated', ''); } // 🔐 共通セッション情報を追加(統一ログアウト用) if (clearSharedSession && this.config.session.shared?.enabled) { params.append('clear_shared_session', '1'); params.append('shared_cookie_domain', this.config.session.shared.cookieDomain); params.append('shared_cookie_name', this.config.session.shared.cookieName); } const logoutUrl = `https://${this.config.authDomain}/v2/logout?${params.toString()}`; log.info(`Generated unified logout URL for service: ${this.config.service.name}`); if (clearSharedSession) { log.info(`Logout will clear shared session across *.gftd.ai domains`); } return logoutUrl; } /** * 認証コールバックURIを取得 */ getCallbackUri() { return `${this.config.service.baseUrl}${this.config.redirects.callbackPath}`; } /** * 設定を取得 */ getConfig() { return { ...this.config }; } /** * サービス固有設定を更新 */ updateServiceConfig(serviceConfig) { this.config.service = { ...this.config.service, ...serviceConfig, }; log.info(`Updated service config for: ${this.config.service.name}`); } /** * 状態データを解析 */ parseStateData(encodedState) { try { const stateData = JSON.parse(Buffer.from(encodedState, 'base64').toString()); return stateData; } catch (error) { log.warn(`Failed to parse state data: ${error}`); return null; } } /** * セッションCookie名を取得 */ getSessionCookieName() { return this.config.session.cookie.name; } /** * セッション設定を取得 */ getSessionConfig() { return { ...this.config.session }; } } /** * 統一認証マネージャーのインスタンスを取得するヘルパー関数 */ export function getUnifiedAuthManager(config) { return UnifiedAuthManager.getInstance(config); } /** * サービス向け統一認証設定のプリセット */ export const UnifiedAuthPresets = { /** * Webmaster (管理画面) 向け設定 - webmaster.gftd.ai */ webmaster: { service: { name: 'GFTD Webmaster', baseUrl: 'https://webmaster.gftd.ai', additionalScopes: ['read:users', 'update:users', 'read:organizations'], }, redirects: { defaultPostLogin: '/projects', defaultPostLogout: '/auth/logout', }, session: { shared: { enabled: true, // *.gftd.ai 共通ログイン有効 }, }, }, /** * CLI向け設定 - cli.gftd.ai */ cli: { service: { name: 'GFTD CLI', baseUrl: 'https://cli.gftd.ai', additionalScopes: ['read:projects', 'write:projects'], }, redirects: { defaultPostLogin: '/auth/success', defaultPostLogout: '/auth/logout', }, session: { shared: { enabled: true, // *.gftd.ai 共通ログイン有効 }, }, }, /** * ORM向け設定 - orm.gftd.ai */ orm: { service: { name: 'GFTD ORM', baseUrl: 'https://orm.gftd.ai', additionalScopes: ['read:data', 'write:data'], }, redirects: { defaultPostLogin: '/', defaultPostLogout: '/', }, session: { shared: { enabled: true, // *.gftd.ai 共通ログイン有効 }, }, }, /** * 開発者向け設定 */ development: { session: { cookie: { secure: false, // 開発環境ではHTTPを許可 }, shared: { enabled: false, // 開発環境では共通ログイン無効 }, }, }, }; /** * Express.js ミドルウェア: 統一認証リダイレクト */ export function unifiedAuthRedirectMiddleware(options = {}) { const { loginPath = '/auth/login', excludePaths = ['/auth/callback', '/auth/logout', '/health', '/api'], autoRedirect = true, } = options; return (req, res, next) => { // 除外パスをチェック const isExcluded = excludePaths.some(path => req.path.startsWith(path)); if (isExcluded) { return next(); } // 認証状態をチェック (簡易版 - 実際はセッション検証が必要) const authManager = getUnifiedAuthManager(); const sessionCookieName = authManager.getSessionCookieName(); const hasSession = req.cookies && req.cookies[sessionCookieName]; if (!hasSession && autoRedirect && req.path === loginPath) { // ログインパスの場合は統一ログインURLにリダイレクト const loginUrl = authManager.buildUnifiedLoginUrl({ returnTo: req.query.returnTo || req.headers.referer || '/', }); return res.redirect(loginUrl); } next(); }; } //# sourceMappingURL=unified-auth-config.js.map