サイトアイコン 協栄情報ブログ

認証の基礎とGoogle Authenticatorの仕組み

みなさん、こんにちは!クラ本部の黒田です。

「またパスワード忘れちゃった…」

「セキュリティ強化しなきゃいけないけど、どうすればいいの?」

そんな悩みを抱えているエンジニアの皆さんのために、今回はGoogle Authenticatorを深掘りしていきましょう!

🎯 目次

  1. はじめに
  2. 認証の基礎知識
  3. Google Authenticatorの仕組み
  4. セキュリティリスクと対策
  5. システム設計のベストプラクティス
  6. 実装の前に知っておくべきこと
  7. トラブルシューティングガイド
  8. まとめと次回予告

1. はじめに 🌟

こんにちは!クラ本部の黒田です。先日、こんな出来事がありました:

「黒田さん、うちのシステムのセキュリティ強化したいんですけど…」
「またパスワード流出のニュースあったじゃないですか…」
「二段階認証って導入難しいですか?」

きっと多くのエンジニアが同じような悩みを抱えているのではないでしょうか?

今回のシリーズでは、Google Authenticatorを深掘りしながら、モダンな認証システムの実装方法を完全解説していきます!

第1回:認証の基礎とGoogle Authenticatorの仕組み(完結編)

1.1 このシリーズで学べること

1.2 想定読者

2. 認証の基礎知識 📚

2.1 認証とは何か

認証って実は私たちの日常生活の至るところに存在します:

これらすべてが「認証」なんです!

2.2 認証の3要素

1. Knowledge:知識認証(Something you know)
   - パスワード
   - PINコード
   - 秘密の質問

2. Possession:所持認証(Something you have)
   - スマートフォン
   - セキュリティキー
   - ICカード

3. Inherence:生体認証(Something you are)
   - 指紋
   - 顔
   - 虹彩

2.3 なぜ二段階認証が必要か

面白い例え話をしましょう:

🏰 お城の防衛システム

当然、防衛が多層になるほど攻略は難しくなりますよね!

3. Google Authenticatorの仕組み 🔍

3.1 TOTPの仕組み

Time-based One-time Password(TOTP)について、詳しく見ていきましょう。

# 疑似コードでTOTPの生成プロセスを表現
def generate_totp(secret_key, timestamp):
    time_step = 30  # 30秒ごとに更新
    counter = floor(timestamp / time_step)

    hmac = HMAC-SHA1(secret_key, counter)
    offset = last_4_bits(hmac)

    # 4バイトを取り出して整数に変換
    binary = extract_4_bytes(hmac, offset)
    otp = binary % 1000000  # 6桁の数字

    return format(otp, '06d')  # ゼロパディング

3.2 認証フローの詳細

Stage 1: 初期設定フェーズ

  1. シークレットキーの生成
    Base32でエンコードされた16-32バイトのランダムな文字列
    例:JBSWY3DPEHPK3PXP
  2. QRコードのURI形式
    otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
  3. データベースへの保存
    CREATE TABLE user_2fa (
       user_id VARCHAR(255) PRIMARY KEY,
       secret_key VARCHAR(255) NOT NULL,
       enabled BOOLEAN DEFAULT true,
       created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

Stage 2: 認証フェーズ

  1. クライアント側の処理
    function generateTOTP(secretKey) {
     const timestamp = Math.floor(Date.now() / 30000);
     // ... TOTPアルゴリズムの実装
    }
  2. サーバー側の検証
    function verifyTOTP(userInput, secretKey) {
     // 前後30秒分のコードも検証(時刻ずれ対策)
     const validCodes = [-1, 0, 1].map(offset => 
       generateTOTP(secretKey, getCurrentTimestamp() + offset)
     );
     return validCodes.includes(userInput);
    }

4. セキュリティリスクと対策 🔒

4.1 主なリスクとその対策

  1. シークレットキーの漏洩
    • 暗号化した状態で保存
    • アクセス制御の徹底
    • 定期的な監査
  2. リプレイアタック
    • 使用済みコードの記録
    • タイムウィンドウの適切な設定
    • レート制限の実装
  3. 時刻同期の問題
    • NTPサーバーの利用
    • 前後の時間窓での検証
    • クライアントへの警告表示

4.2 セキュリティ強化のTips

class TOTPValidator {
  private readonly usedCodes: Set<string> = new Set();
  private readonly cleanupInterval = 60000; // 1分

  constructor() {
    // 使用済みコードの定期クリーンアップ
    setInterval(() => this.cleanupUsedCodes(), this.cleanupInterval);
  }

  validateCode(code: string, secretKey: string): boolean {
    if (this.usedCodes.has(code)) {
      return false; // リプレイアタック対策
    }

    const isValid = this.verifyTOTP(code, secretKey);
    if (isValid) {
      this.usedCodes.add(code);
    }

    return isValid;
  }
}

5. システム設計のベストプラクティス 🎨

5.1 アーキテクチャ設計

マイクロサービスアーキテクチャの例

クラウドネイティブな実装例(AWS)

# serverless.yml
service: two-factor-auth

provider:
  name: aws
  runtime: nodejs16.x

functions:
  generateSecret:
    handler: src/handlers/generate.handler
    events:
      - http:
          path: /2fa/setup
          method: post
          authorizer: aws_iam

  verifyToken:
    handler: src/handlers/verify.handler
    events:
      - http:
          path: /2fa/verify
          method: post

5.2 データモデル設計

// ユーザー認証情報のインターフェース
interface IUserAuth {
  userId: string;
  email: string;
  secretKey: string;
  backupCodes: string[];
  isEnabled: boolean;
  lastUsed: Date;
  recoveryOptions: {
    email: boolean;
    phone: boolean;
  };
}

// 認証履歴のインターフェース
interface IAuthHistory {
  userId: string;
  timestamp: Date;
  action: 'SETUP' | 'VERIFY' | 'DISABLE';
  status: 'SUCCESS' | 'FAILURE';
  metadata: {
    ipAddress: string;
    userAgent: string;
    location?: string;
  };
}

5.3 スケーラビリティ設計

class AuthenticatorService {
  private readonly cache: Redis;
  private readonly db: Database;
  private readonly metrics: MetricsService;

  async verifyToken(userId: string, token: string): Promise<boolean> {
    // レート制限のチェック
    const attempts = await this.cache.incr(auth:${userId}:attempts);
    if (attempts > MAX_ATTEMPTS) {
      throw new RateLimitExceededError();
    }

    try {
      const user = await this.cache.get(user:${userId});
      if (!user) {
        const dbUser = await this.db.users.findById(userId);
        await this.cache.set(user:${userId}, dbUser, 'EX', 300);
      }

      // 検証ロジック
      return this.validateTOTP(user.secretKey, token);
    } finally {
      // メトリクス記録
      this.metrics.recordAuthAttempt(userId, {
        success: true,
        latency: Date.now() - startTime,
      });
    }
  }
}

6. 実装の前に知っておくべきこと 📝

6.1 環境構築チェックリスト

# 開発環境セットアップ
- [ ] Node.js v16以上
- [ ] AWS CLIのインストールと設定
- [ ] TypeScriptの開発環境
- [ ] 必要なAWS権限の確認

# 必要なパッケージ
- [ ] @aws-sdk/client-dynamodb
- [ ] otplib
- [ ] qrcode
- [ ] aws-lambda

6.2 必要なAWSリソース

# main.tf
resource "aws_dynamodb_table" "two_factor_auth" {
  name           = "two-factor-auth"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "userId"
  range_key      = "timestamp"

  attribute {
    name = "userId"
    type = "S"
  }

  attribute {
    name = "timestamp"
    type = "N"
  }

  ttl {
    enabled        = true
    attribute_name = "expiresAt"
  }
}

resource "aws_kms_key" "secret_encryption" {
  description = "KMS key for 2FA secret encryption"
  enable_key_rotation = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "*"
        }
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })
}

7. トラブルシューティングガイド 🔧

7.1 よくある問題と解決方法

時刻同期の問題

class TOTPValidator {
  private static readonly TIME_SKEW = 1; // ±1ステップを許容

  validateWithSkew(token: string, secret: string): boolean {
    const currentWindow = Math.floor(Date.now() / 30000);

    // 前後の時間窓もチェック
    for (let i = -this.TIME_SKEW; i <= this.TIME_SKEW; i++) {
      const expectedToken = this.generateTOTP(secret, currentWindow + i);
      if (token === expectedToken) {
        if (i !== 0) {
          console.warn(Time skew detected: ${i * 30} seconds);
        }
        return true;
      }
    }
    return false;
  }

  private detectTimeSkew(clientTime: number): void {
    const serverTime = Date.now();
    const skew = Math.abs(clientTime - serverTime);

    if (skew > 30000) { // 30秒以上のずれ
      console.error(Significant time skew detected: ${skew}ms);
      // アラート送信やログ記録
    }
  }
}

エラーハンドリング実装例

class AuthError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly httpStatus: number
  ) {
    super(message);
  }
}

class AuthenticationService {
  async verify(userId: string, token: string): Promise<Result<boolean>> {
    try {
      // 基本的なバリデーション
      if (!this.isValidTokenFormat(token)) {
        throw new AuthError(
          'Invalid token format',
          'INVALID_TOKEN_FORMAT',
          400
        );
      }

      // レート制限チェック
      if (await this.isRateLimited(userId)) {
        throw new AuthError(
          'Too many attempts',
          'RATE_LIMIT_EXCEEDED',
          429
        );
      }

      // 実際の検証
      const result = await this.validateToken(userId, token);
      return Result.success(result);

    } catch (error) {
      // エラーログ記録
      await this.logError(error, { userId });

      // クライアントへの適切なエラーレスポンス
      if (error instanceof AuthError) {
        return Result.failure(error);
      }

      return Result.failure(new AuthError(
        'Internal server error',
        'INTERNAL_ERROR',
        500
      ));
    }
  }
}

7.2 デバッグとモニタリング

CloudWatchメトリクス設定

class MetricsService {
  private readonly cloudwatch: CloudWatch;

  async recordMetrics(userId: string, metrics: AuthMetrics): Promise<void> {
    await this.cloudwatch.putMetricData({
      Namespace: 'TwoFactorAuth',
      MetricData: [
        {
          MetricName: 'AuthenticationAttempt',
          Value: 1,
          Unit: 'Count',
          Dimensions: [
            {
              Name: 'UserId',
              Value: userId
            },
            {
              Name: 'Status',
              Value: metrics.success ? 'Success' : 'Failure'
            }
          ]
        },
        {
          MetricName: 'AuthenticationLatency',
          Value: metrics.latency,
          Unit: 'Milliseconds'
        }
      ]
    });
  }
}

アラート設定例(CloudFormation)

Resources:
  FailedAuthAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: TwoFactorAuth-FailedAttempts
      MetricName: AuthenticationAttempt
      Namespace: TwoFactorAuth
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 10
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: Status
          Value: Failure
      AlarmActions:
        - !Ref AlertSNSTopic

8. まとめと次回予告 🎯

8.1 本日のまとめ

8.2 実践演習課題

// 課題1: 以下のTOTP実装をセキュアに改善してください
class BasicTOTP {
  generateToken(secret: string): string {
    const counter = Math.floor(Date.now() / 30000);
    return this.calculateTOTP(secret, counter);
  }
}

// 課題2: バックアップコード機能を追加してください
interface BackupCodeService {
  generateCodes(userId: string): string[];
  validateCode(userId: string, code: string): Promise<boolean>;
}

8.3 次回予告

第2回では「OAuth2.0とGoogle認証の実装」と題して、以下の内容をお届けします:

参考リソース 📚

いかがでしたでしょうか?次回の第2回では、より実践的な実装に踏み込んでいきます。ご質問やフィードバックがございましたら、コメント欄にてお待ちしております!

モバイルバージョンを終了