メインコンテンツまでスキップ

Session Authentication

@zeltjs/auth-session は、サーバーレンダリングアプリケーションのためのcookieベースのセッション管理を提供します。

インストール

pnpm add @zeltjs/auth-session @zeltjs/kv

クイックスタート

1. secretを設定する

SESSION_SECRET 環境変数を設定します。

# .env
SESSION_SECRET=your-secret-key-at-least-32-characters

2. セッションストアを設定する

セッションはKVストアに保存されます。デフォルトでは SessionConfigsession: namespace配下でin-memory adaptorを使用します。namespace(やその他のオプション)をカスタマイズするには SessionConfig を継承します。

@Config
class MySessionConfig extends SessionConfig {
  override readonly kvStoreNamespace = 'sessions:';
}

3. middlewareを登録する

const app = createApp([http({
    controllers: [AuthController, UserController],
    middlewares: [SessionMiddleware],
  })], { configs: [MySessionConfig] });

4. セッションを管理する

ハンドラー内でセッション関数を使用します。

@Controller('/auth')
class AuthController {
  @Post('/login')
  async login(req = request(LoginSchema)) {
    const body = await req.body();
    const user = await validateCredentials(body.email, body.password);
    if (!user) {
      throw new HTTPException(401, { message: 'Invalid credentials' });
    }
    
    setSession({ userId: user.id, name: user.name });
    return { success: true };
  }

  @Get('/me')
  me() {
    const session = getSession();
    if (!session) {
      throw new HTTPException(401, { message: 'Not logged in' });
    }
    return session;
  }

  @Post('/logout')
  logout() {
    destroySession();
    return { success: true };
  }
}

Session API

関数説明
getSession()現在のセッションデータを取得する(未ログインなら undefined)
setSession(data)セッションデータを設定する(既存のものを置き換える)
updateSession(fn)関数でセッションデータを更新する
destroySession()セッションを破棄しcookieをクリアする
isNewSession()新規に作成されたセッションかを確認する
getSessionId()現在のセッションIDを取得する

setSession

セッションを作成または置き換えます。

setSession({
  userId: '123',
  name: 'Alice',
  cart: [{ productId: 'abc', qty: 2 }],
});

updateSession

セッションを部分的に更新します。

updateSession((session) => ({
  ...session,
  lastActivity: Date.now(),
}));

destroySession

セッションとcookieをクリアします(ログアウト用)。

destroySession();

型安全なセッション

SessionSchema を継承すると、型安全にセッションへアクセスできます。

declare module '@zeltjs/auth-session' {
  interface SessionSchema {
    userId?: string;
    name?: string;
    email?: string;
    cart?: CartItem[];
  }
}

これで、すべてのセッション関数に型が付きます。

const session = getSession();
// TypeScriptはsession?.userId、session?.name、session?.cartを認識する

setSession({ userId: '123', name: 'Alice' });
// SessionSchemaに対して型チェックされる

設定

SessionConfig を継承して動作をカスタマイズします。

@Config
class MySessionConfig extends SessionConfig {
  override readonly kvStoreNamespace = 'sessions:';

  override get cookieName(): string {
    return 'my_session';  // デフォルト: 'session'
  }

  override get ttlSec(): number {
    return 86400 * 7;  // 7日(デフォルト: 1日)
  }

  override get cookieOptions() {
    return {
      httpOnly: true,
      secure: true,
      sameSite: 'Strict' as const,
      path: '/',
    };
  }
}

Configuration Options

オプションデフォルト説明
kvKVAdaptorMemoryKVセッションストレージの裏側となるKV adaptor(コンストラクタ引数2)
kvStoreNamespacestring'session:'セッションkeyのnamespace prefix
secretstringenv.getString('SESSION_SECRET')セッションID署名用のsecret
cookieNamestring'session'cookie名
ttlSecnumber86400(1日)セッションのTTL(秒)
cookieOptionsobject下記参照cookieの設定
  override get cookieOptions() {
    return {
      httpOnly: true,
      secure: this.env.getString('NODE_ENV', '') === 'production',
      sameSite: 'Lax' as const,
      path: '/',
    };
  }
}

ストレージバックエンド

Memory(開発用)

@Config
class MySessionConfig extends SessionConfig {
  constructor(kv = inject(MemoryKV)) {
    super(undefined, kv);
  }
}

Redis(本番用)

SessionConfig はKV adaptorをコンストラクタの第2引数として受け取ります。セッションをRedisに保存するには RedisKVAdaptorsuper() に渡します(第1引数はデフォルトの Env injectionを維持するため undefined のままにします)。

@Config
class MySessionConfig extends SessionConfig {
  constructor(kv = inject(RedisKVAdaptor)) {
    super(undefined, kv);
  }

  override readonly kvStoreNamespace = 'sessions:';
}

Redisを使用するには、adaptorが接続を解決できるよう(@zeltjs/redis の)RedisConfig を登録する必要があります。

User Contextとの統合

セッションは自動的にuser contextを設定しません。両者をつなぐmiddlewareを追加します。

@Middleware
export class SessionAuthMiddleware {
  constructor(private userRepo = inject(UserRepository)) {}

  async use(next: Next): Promise<Response | undefined> {
    const session = getSession() as { userId?: string } | undefined;

    if (session?.userId) {
      const user = await this.userRepo.findById(session.userId);
      setUser(
        { id: user.id, name: user.name, email: user.email },
        user.roles
      );
    }

    await next();
    return undefined;
  }
}

SessionMiddleware の後に登録します。

const app = createApp([http({
    controllers: [UserController],
    middlewares: [SessionMiddleware, SessionAuthMiddleware],
  })], { configs: [MySessionConfig] });

セキュリティ上の注意点

CSRF Protection

セッションベースの認証にはCSRF対策が必要です。以下の方法を検討してください。

  • SameSite=Strict cookie(最も強力だが、UXに影響する場合がある)
  • 変更を伴う操作に対する SameSite=Lax cookie + CSRFトークン
  • Double-submit cookieパターン

Session Fixation

ログイン後は必ずセッションIDを再生成してください。

@Controller('/auth')
class AuthController {
  @Post('/login')
  async login(req = request(LoginSchema)) {
    const body = await req.body();
    const user = await validateCredentials(body.email, body.password);
    
    destroySession();  // 古いセッションをクリア
    setSession({ userId: user.id, name: user.name });  // 新しいIDを作成
    
    return { success: true };
  }
}

Secure Cookies

本番環境では、必ずsecure cookieを使用してください。

const cookieOptions = {
  httpOnly: true,
  secure: true,  // HTTPSのみ
  sameSite: 'Strict' as const,
  path: '/',
};