0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 11: Auth Serviceアーキテクチャ

Last updated at Posted at 2025-12-10

🎄 科学と神々株式会社 アドベントカレンダー 2025

Hybrid License System Day 11: Auth Serviceアーキテクチャ

Auth Service編 (1/5)


📖 はじめに

Day 11では、Auth Serviceのアーキテクチャと実装を学びます。ライセンス認証を担う中核サービスの設計思想と、JavaScript/Node.jsによる実装を理解しましょう。


🏗️ Auth Serviceの責務

Auth Serviceは、Hybrid License Systemのセキュリティの要として、以下の責務を担います:

┌──────────────────────────────────────┐
│  Auth Service (Port 3001)            │
│                                      │
│  ┌────────────────────────────────┐  │
│  │ Responsibilities               │  │
│  │                                │  │
│  │ 1. ライセンスアクティベーション  │  │
│  │ 2. ライセンス検証               │  │
│  │ 3. JWT生成・検証                │  │
│  │ 4. BCryptパスワードハッシング    │  │
│  │ 5. データベース操作(SQLite)    │  │
│  └────────────────────────────────┘  │
│                                      │
│  Technology Stack:                   │
│  - Node.js 20+                       │
│  - better-sqlite3 (Database)         │
│  - bcryptjs (Password Hashing)       │
│  - jsonwebtoken (JWT)                │
└──────────────────────────────────────┘

🔐 なぜJavaScript/Node.jsなのか?

実装言語の選択理由

元々はNim言語での実装を想定していましたが、以下の理由でJavaScript/Node.jsに変更しました:

  1. エコシステムの豊富さ: npm/yarnによる豊富なライブラリ
  2. 開発速度: 迅速なプロトタイピングと実装
  3. チーム互換性: フロントエンド・バックエンド統一言語
  4. デプロイの簡単さ: Docker/Kubernetesとの親和性
  5. コミュニティサポート: Express.js/bcrypt/JWTの成熟したエコシステム

📁 プロジェクト構造

auth-service/
├── src/
│   └── standalone.js       # 統合実装(サーバー・認証・DB・暗号化)
├── package.json            # 依存関係定義
├── Dockerfile              # Docker設定
└── README.md               # サービスドキュメント

設計思想: シンプルさと保守性を優先し、全機能をstandalone.jsに統合。マイクロサービスとして独立性を保ちながら、内部構造は単純化しています。


🛠️ サービス実装(統合版)

完全なstandalone.js実装

Auth Serviceの全機能を1ファイルに統合した実装です。

// auth-service/src/standalone.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const Database = require('better-sqlite3');
const path = require('path');

const app = express();
const PORT = process.env.AUTH_SERVICE_PORT || 3001;
const JWT_SECRET = process.env.JWT_SECRET || 'test-secret-key-for-development-minimum-32-characters-long';
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../shared/database/licenses.db');

// Middleware
app.use(express.json());

// データベース初期化
let db;
try {
  db = new Database(DB_PATH);
  console.log('✅ Database connected:', DB_PATH);

  // デモアカウント作成(初回のみ)
  const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
  if (userCount === 0) {
    console.log('📝 Seeding demo accounts...');
    seedDemoAccounts();
  }
} catch (error) {
  console.error('❌ Database connection failed:', error.message);
  process.exit(1);
}

// デモアカウント作成
function seedDemoAccounts() {
  const hashedPassword = bcrypt.hashSync('password123', 12);
  const now = new Date().toISOString();

  const accounts = [
    { email: 'free@example.com', plan: 'free' },
    { email: 'premium@example.com', plan: 'premium_monthly' },
    { email: 'enterprise@example.com', plan: 'enterprise_monthly' }
  ];

  accounts.forEach(acc => {
    const userId = `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

    db.prepare(`
      INSERT OR IGNORE INTO users (user_id, email, password_hash, created_at, is_active, role)
      VALUES (?, ?, ?, ?, 1, 'user')
    `).run(userId, acc.email, hashedPassword, now);

    const subscriptionId = `sub-${userId}`;
    db.prepare(`
      INSERT OR IGNORE INTO subscriptions (subscription_id, user_id, plan_type, status, start_date, end_date)
      VALUES (?, ?, ?, 'active', ?, datetime('now', '+1 year'))
    `).run(subscriptionId, userId, acc.plan, now);
  });

  console.log('✅ Demo accounts seeded');
}

// ===== エンドポイント =====

// ヘルスチェック
app.get('/health', (req, res) => {
  res.json({
    service: 'auth-service',
    status: 'healthy',
    version: '1.0.0',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// ライセンスアクティベーション
app.post('/api/v1/license/activate', (req, res) => {
  try {
    const { email, password, client_id } = req.body;

    // 入力検証
    if (!email || !password || !client_id) {
      return res.status(400).json({
        success: false,
        error: { code: 'MISSING_FIELDS', message: 'Email, password, and client_id are required' }
      });
    }

    // ユーザー取得
    const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
    if (!user) {
      return res.status(401).json({
        success: false,
        error: { code: 'INVALID_CREDENTIALS', message: 'Invalid email or password' }
      });
    }

    // パスワード検証(BCrypt)
    if (!bcrypt.compareSync(password, user.password_hash)) {
      return res.status(401).json({
        success: false,
        error: { code: 'INVALID_CREDENTIALS', message: 'Invalid email or password' }
      });
    }

    // アクティブなサブスクリプション取得
    const subscription = db.prepare(`
      SELECT * FROM subscriptions
      WHERE user_id = ? AND status = 'active'
      ORDER BY start_date DESC LIMIT 1
    `).get(user.user_id);

    if (!subscription) {
      return res.status(403).json({
        success: false,
        error: { code: 'NO_ACTIVE_SUBSCRIPTION', message: 'No active subscription found' }
      });
    }

    // JWT生成(HS256)
    const payload = {
      userId: user.user_id,
      email: user.email,
      plan: subscription.plan_type,
      clientId: client_id,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60) // 1年
    };

    const activationKey = jwt.sign(payload, JWT_SECRET);

    // ライセンス作成/更新
    const now = new Date().toISOString();
    const existingLicense = db.prepare(`
      SELECT license_id, activation_key FROM licenses
      WHERE subscription_id = ? AND client_id = ?
    `).get(subscription.subscription_id, client_id);

    let finalActivationKey;
    let finalLicenseId;

    if (existingLicense) {
      // 既存ライセンス更新
      finalActivationKey = existingLicense.activation_key;
      finalLicenseId = existingLicense.license_id;
      db.prepare(`
        UPDATE licenses
        SET activated_at = ?, last_validated = ?, is_active = 1
        WHERE license_id = ?
      `).run(now, now, finalLicenseId);
    } else {
      // 新規ライセンス作成
      finalLicenseId = `license-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      finalActivationKey = activationKey;
      db.prepare(`
        INSERT INTO licenses (license_id, subscription_id, client_id, activation_key, activated_at, last_validated, is_active)
        VALUES (?, ?, ?, ?, ?, ?, 1)
      `).run(finalLicenseId, subscription.subscription_id, client_id, finalActivationKey, now, now);
    }

    res.json({
      success: true,
      data: {
        activation_key: finalActivationKey,
        license_id: finalLicenseId,
        user: {
          userId: user.user_id,
          email: user.email
        },
        subscription: {
          plan: subscription.plan_type,
          status: subscription.status,
          endDate: subscription.end_date
        }
      }
    });
  } catch (error) {
    console.error('Activation error:', error);
    res.status(500).json({
      success: false,
      error: { code: 'INTERNAL_ERROR', message: 'An error occurred during activation' }
    });
  }
});

// ライセンス検証
app.get('/api/v1/license/validate', (req, res) => {
  try {
    const authHeader = req.headers.authorization;
    const token = authHeader?.split(' ')[1];

    if (!token) {
      return res.status(401).json({
        success: false,
        error: { code: 'MISSING_TOKEN', message: 'Authorization token is required' }
      });
    }

    // JWT検証(HS256)
    const decoded = jwt.verify(token, JWT_SECRET);

    // ライセンスの有効性確認
    const license = db.prepare(`
      SELECT l.*, s.status as subscription_status, s.end_date
      FROM licenses l
      JOIN subscriptions s ON l.subscription_id = s.subscription_id
      WHERE l.activation_key = ? AND l.is_active = 1
    `).get(token);

    if (!license) {
      return res.status(401).json({
        success: false,
        error: { code: 'LICENSE_NOT_FOUND', message: 'License not found or inactive' }
      });
    }

    // サブスクリプション期限確認
    if (license.subscription_status !== 'active') {
      return res.status(403).json({
        success: false,
        error: { code: 'SUBSCRIPTION_INACTIVE', message: 'Subscription is no longer active' }
      });
    }

    // 最終検証時刻更新
    db.prepare('UPDATE licenses SET last_validated = ? WHERE license_id = ?')
      .run(new Date().toISOString(), license.license_id);

    res.json({
      success: true,
      data: {
        valid: true,
        userId: decoded.userId,
        email: decoded.email,
        plan: decoded.plan,
        clientId: decoded.clientId,
        expiresAt: license.end_date
      }
    });
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({
        success: false,
        error: { code: 'TOKEN_EXPIRED', message: 'Token has expired' }
      });
    }

    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({
        success: false,
        error: { code: 'INVALID_TOKEN', message: 'Invalid token' }
      });
    }

    console.error('Validation error:', error);
    res.status(500).json({
      success: false,
      error: { code: 'INTERNAL_ERROR', message: 'An error occurred during validation' }
    });
  }
});

// サービスエコーテスト(開発用)
app.post('/api/v1/service/echo', (req, res) => {
  res.json({
    success: true,
    data: {
      message: 'Echo from Auth Service',
      receivedPayload: req.body,
      timestamp: new Date().toISOString()
    }
  });
});

// サーバー起動
app.listen(PORT, () => {
  console.log(`✅ Auth Service running on port ${PORT}`);
});

module.exports = app;

🔑 主要機能の実装詳細

1. データベース操作(better-sqlite3)

// SQLiteデータベース接続
const db = new Database(DB_PATH);

// プリペアドステートメント(SQLインジェクション対策)
const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);

// トランザクション処理
const insertLicense = db.transaction((subscriptionId, clientId, activationKey) => {
  const licenseId = `license-${Date.now()}`;
  db.prepare(`
    INSERT INTO licenses (license_id, subscription_id, client_id, activation_key, activated_at, is_active)
    VALUES (?, ?, ?, ?, ?, 1)
  `).run(licenseId, subscriptionId, clientId, activationKey, new Date().toISOString());

  return licenseId;
});

2. パスワードハッシング(BCrypt)

// パスワードハッシュ化(cost factor 12)
const hashedPassword = bcrypt.hashSync('password123', 12);

// パスワード検証(タイミング攻撃対策)
const isValid = bcrypt.compareSync(password, user.password_hash);

3. JWT生成・検証(HS256)

// JWT生成
const payload = {
  userId: user.user_id,
  email: user.email,
  plan: subscription.plan_type,
  clientId: client_id,
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + (365 * 24 * 60 * 60) // 1年
};

const token = jwt.sign(payload, JWT_SECRET);

// JWT検証
try {
  const decoded = jwt.verify(token, JWT_SECRET);
  console.log('Token valid:', decoded);
} catch (error) {
  if (error.name === 'TokenExpiredError') {
    console.error('Token expired');
  }
}

📊 データベーススキーマ

テーブル構造

-- ユーザーテーブル
CREATE TABLE users (
  user_id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TEXT NOT NULL,
  is_active INTEGER DEFAULT 1,
  role TEXT DEFAULT 'user'
);

-- サブスクリプションテーブル
CREATE TABLE subscriptions (
  subscription_id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  plan_type TEXT NOT NULL,
  status TEXT DEFAULT 'active',
  start_date TEXT NOT NULL,
  end_date TEXT,
  FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- ライセンステーブル
CREATE TABLE licenses (
  license_id TEXT PRIMARY KEY,
  subscription_id TEXT NOT NULL,
  client_id TEXT NOT NULL,
  activation_key TEXT NOT NULL,
  activated_at TEXT,
  last_validated TEXT,
  is_active INTEGER DEFAULT 1,
  FOREIGN KEY (subscription_id) REFERENCES subscriptions(subscription_id),
  UNIQUE(subscription_id, client_id)
);

🔒 セキュリティ考慮事項

1. パスワードセキュリティ

  • ✅ BCrypt cost factor 12(適切な計算コスト)
  • ✅ タイミング攻撃対策(compareSync
  • ✅ パスワードハッシュの不可逆性

2. JWT セキュリティ

  • ✅ HS256署名アルゴリズム
  • ✅ 有効期限設定(1年)
  • ✅ トークン検証時の例外ハンドリング

3. データベースセキュリティ

  • ✅ プリペアドステートメント(SQLインジェクション対策)
  • ✅ 外部キー制約
  • ✅ UNIQUE制約(重複防止)

4. 入力検証

  • ✅ 必須フィールドチェック
  • ✅ メールアドレス形式検証(クライアント側)
  • ✅ エラーメッセージの適切な抽象化

🎯 次のステップ

Day 12では、ライセンスアクティベーション実装を学びます。ユーザー認証フロー、ライセンス生成ロジック、トランザクション処理について詳しく解説します。


🔗 関連リンク


次回予告: Day 12では、アクティベーションフローとデータベーストランザクションを詳しく解説します!


Copyright © 2025 Gods & Golem, Inc. All rights reserved.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?