@gftdcojp/auth
Version:
✅ Enterprise-grade Auth0 integration for GFTD platform - 90% Complete, High Quality Implementation
235 lines • 8.53 kB
JavaScript
"use strict";
/**
* セッション管理ユーティリティ
*
* 機能:
* - セッションの暗号化/復号化
* - セッション有効期限チェック
* - セッション更新
* - セキュアなCookie設定
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionManager = void 0;
const jose_1 = require("jose");
const logger_1 = require("./logger");
/**
* セッション管理クラス
*/
class SessionManager {
constructor(config) {
this.config = config;
// セッション暗号化キーを準備
const secret = config.secret;
if (!secret || secret.length < 32) {
throw new Error('AUTH0_SECRET must be at least 32 characters long');
}
this.secretKey = new TextEncoder().encode(secret.slice(0, 32));
}
/**
* セッションを暗号化してトークンにする
*/
async encryptSession(session) {
try {
const jwt = await new jose_1.EncryptJWT({
user: session.user,
idToken: session.idToken,
accessToken: session.accessToken,
refreshToken: session.refreshToken,
expiresAt: session.expiresAt,
createdAt: session.createdAt,
organizationContext: session.organizationContext,
})
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime(session.expiresAt)
.setIssuer('gftd-auth')
.setAudience(this.config.appBaseUrl)
.encrypt(this.secretKey);
return jwt;
}
catch (error) {
logger_1.log.error(`Failed to encrypt session: ${error}`);
throw new Error('Session encryption failed');
}
}
/**
* 暗号化されたトークンからセッションを復号化する
*/
async decryptSession(encryptedToken) {
try {
const { payload } = await (0, jose_1.jwtDecrypt)(encryptedToken, this.secretKey, {
issuer: 'gftd-auth',
audience: this.config.appBaseUrl,
});
// 有効期限チェック
const now = Date.now();
if (payload.expiresAt && typeof payload.expiresAt === 'number' && payload.expiresAt < now) {
logger_1.log.warn('Session expired');
return null;
}
return {
user: payload.user,
idToken: payload.idToken,
accessToken: payload.accessToken,
refreshToken: payload.refreshToken,
expiresAt: payload.expiresAt,
createdAt: payload.createdAt,
organizationContext: payload.organizationContext,
};
}
catch (error) {
logger_1.log.error(`Failed to decrypt session: ${error}`);
return null;
}
}
/**
* リクエストからセッションを取得
*/
async getSessionFromRequest(request) {
const sessionCookie = request.cookies.get('gftd_session');
if (!sessionCookie?.value) {
return null;
}
return this.decryptSession(sessionCookie.value);
}
/**
* レスポンスにセッションを設定
*/
async setSessionInResponse(response, session) {
if (session) {
const encryptedSession = await this.encryptSession(session);
// セキュアなCookie設定
response.cookies.set('gftd_session', encryptedSession, {
httpOnly: true,
secure: this.config.session?.cookie?.secure ?? (process.env.NODE_ENV === 'production'),
sameSite: this.config.session?.cookie?.sameSite ?? 'lax',
path: this.config.session?.cookie?.path ?? '/',
domain: this.config.session?.cookie?.domain,
maxAge: this.config.session?.absoluteLifetime ?? (7 * 24 * 60 * 60), // 7日
});
}
else {
// セッションクリア
response.cookies.delete('gftd_session');
}
}
/**
* セッション更新(ローリングセッション対応)
*/
async updateSession(session) {
const now = Date.now();
// ローリングセッションが有効な場合
if (this.config.session?.rolling) {
const rollingDuration = this.config.session.rollingDuration ?? (24 * 60 * 60 * 1000); // 24時間
session.expiresAt = now + rollingDuration;
}
return session;
}
/**
* セッションが有効かチェック
*/
isSessionValid(session) {
if (!session)
return false;
const now = Date.now();
// 有効期限チェック
if (session.expiresAt < now) {
return false;
}
// 絶対有効期限チェック
if (this.config.session?.absoluteLifetime) {
const absoluteExpiry = session.createdAt + (this.config.session.absoluteLifetime * 1000);
if (absoluteExpiry < now) {
return false;
}
}
return true;
}
/**
* 🆕 組織コンテキストでのセッション検証
*/
validateOrganizationContext(session, organizationId) {
if (!session)
return false;
// 組織が必須の場合
if (this.config.organization?.requireOrganization && !session.user.organization_id) {
return false;
}
// 特定組織のチェック
if (organizationId && session.user.organization_id !== organizationId) {
return false;
}
return true;
}
/**
* セッションからアクセストークンを取得(リフレッシュも含む)
*/
async getAccessToken(session) {
if (!session)
return null;
// トークンがまだ有効な場合
const tokenExpiry = this.extractTokenExpiry(session.accessToken);
const now = Date.now();
if (tokenExpiry && tokenExpiry > now + 60000) { // 1分のバッファ
return { accessToken: session.accessToken, session };
}
// リフレッシュトークンがある場合はリフレッシュを試行
if (session.refreshToken) {
const refreshedTokens = await this.refreshAccessToken(session.refreshToken);
if (refreshedTokens) {
const updatedSession = {
...session,
accessToken: refreshedTokens.accessToken,
refreshToken: refreshedTokens.refreshToken || session.refreshToken,
expiresAt: now + (refreshedTokens.expiresIn * 1000),
};
return { accessToken: refreshedTokens.accessToken, session: updatedSession };
}
}
return null;
}
/**
* アクセストークンをリフレッシュ
*/
async refreshAccessToken(refreshToken) {
try {
const response = await fetch(`https://${this.config.domain}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
refresh_token: refreshToken,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const tokens = await response.json();
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in || 3600,
};
}
catch (error) {
logger_1.log.error(`Failed to refresh access token: ${error}`);
return null;
}
}
/**
* JWTトークンから有効期限を抽出
*/
extractTokenExpiry(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp ? payload.exp * 1000 : null;
}
catch {
return null;
}
}
}
exports.SessionManager = SessionManager;
//# sourceMappingURL=session-manager.js.map