@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
406 lines • 15.3 kB
JavaScript
/**
* 統一認証設定クラス - 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