🎄 科学と神々株式会社 アドベントカレンダー 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に変更しました:
- エコシステムの豊富さ: npm/yarnによる豊富なライブラリ
- 開発速度: 迅速なプロトタイピングと実装
- チーム互換性: フロントエンド・バックエンド統一言語
- デプロイの簡単さ: Docker/Kubernetesとの親和性
- コミュニティサポート: 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.