メインコンテンツにスキップ

認証処理の重複を解決!

はじめに

今回は、AWS Lambda関数(サーバーを管理することなくコードを実行できるAWSのサービス) で発生していた 認証処理の重複問題 をTDD(Test-Driven Development:テスト駆動開発)で解決した事例をご紹介します。

🚨 問題:認証処理が各所で重複していた

何が問題だったのか?

私たちのプロジェクトでは、複数のLambda関数で認証が必要でした。しかし、各関数で 同じような認証処理を個別に実装 してしまっていました。

具体的な重複例

❌ 問題のあったコード例(cmd/hello/main.go)

func businessHandler(ctx context.Context, request events.APIGateWayProxyRequest, log *logger.Logger, metricsClient *middleware.MetricsClient) (events.APIGateWayProxyResponse, error) {
    // 🔴 手動で認証ヘッダーを解析
    authHeader := request.Headers["Authorization"]
    if authHeader == "" {
        // Case-insensitiveで再チェック
        for key, value := range request.Headers {
            if strings.ToLower(key) == "authorization" {
                authHeader = value
                break
            }
        }
    }
    
    if authHeader == "" {
        return utils.CreateErrorResponse(401, "Unauthorized", "Authorization header missing"), nil
    }
    
    // 🔴 Bearerトークンの解析も手動
    if !strings.HasPrefix(authHeader, "Bearer ") { // Bearerトークンは、認証情報を含むHTTPヘッダーの一種で、通常は「Bearer [トークン文字列]」の形式です。
        return utils.CreateErrorResponse(401, "Unauthorized", "Invalid authorization header format"), nil
    }
    
    accessToken := strings.TrimPrefix(authHeader, "Bearer ")
    if accessToken == "" {
        return utils.CreateErrorResponse(401, "Unauthorized", "Empty token"), nil
    }
    
    // ... 実際のビジネスロジック
}

同じような処理が他の関数にも…

  • cmd/update-user/main.go にも同様のコード
  • cmd/admin-users/main.go にも同様のコード
  • cmd/delete-user/main.go にも同様のコード

何が悪かったのか?

  1. コードの重複: 同じ処理を複数箇所で実装しているため、コード量が増え、見通しが悪くなります。
  2. 保守性の低下: 認証ロジックに修正が必要になった場合、すべてのLambda関数を個別に変更する必要があり、手間がかかります。
  3. バグの温床: 各関数で個別に実装しているため、実装に差異が生まれやすく、それがバグの原因となるリスクが高まります。
  4. テストの複雑化: 各関数で認証部分のテストを個別に書く必要があり、テストコードの量も増え、管理が大変になります。

🎯 解決方針:TDDで統一的な認証処理に改善

TDDとは?

TDD(Test-Driven Development:テスト駆動開発) は以下のサイクルで開発を進める手法です:

  1. Red: 最初に失敗するテストを書きます。まだ実装されていない機能に対するテストなので、当然失敗します。
  2. Green: そのテストを通すための最小限の実装を行います。余計な機能は追加せず、テストが成功することだけを目指します。
  3. Refactor: テストが通ったことを確認したら、コードを改善(リファクタリング)します。機能は変えずに、コードの可読性や保守性を高めます。

このサイクルを繰り返すことで、高品質なコードを効率的に開発できます。

改善計画

  1. 統一認証関数の作成: 認証処理を一箇所にまとめた utils.ExtractTokenFromRequest() 関数を作成します。
  2. 各Lambda関数のリファクタリング: 各Lambda関数から重複している認証コードを削除し、作成した統一認証関数に置き換えます。
  3. テストでの品質確保: 統一認証関数に対してテストを徹底的に行い、一貫性のあるエラーハンドリングが実現されていることを確認します。

🔧 実装:ステップバイステップで改善

Step 1: 統一認証関数の確認

まず、既存の統一認証関数を確認しました:

✅ 統一認証関数(pkg/utils/auth.go)

// 統一されたトークン抽出関数
func ExtractTokenFromRequest(request events.APIGatewayProxyRequest) (string, error) {
    authHeader := GetAuthorizationHeader(request.Headers)
    if authHeader == "" {
        return "", ErrAuthHeaderNotFound
    }
    
    if authHeader == "" {
        return "", ErrEmptyAuthHeader
    }
    
    if !strings.HasPrefix(authHeader, "Bearer ") {
        return "", ErrInvalidAuthFormat
    }
    
    token := strings.TrimPrefix(authHeader, "Bearer ")
    if token == "" {
        return "", ErrEmptyToken
    }
    
    return token, nil
}

// Case-insensitiveでAuthorizationヘッダーを取得
func GetAuthorizationHeader(headers map[string]string) string {
    // 通常のケース
    if auth := headers["Authorization"]; auth != "" {
        return auth
    }
    
    // Case-insensitiveでの検索
    for key, value := range headers {
        if strings.EqualFold(key, "authorization") {
            return value
        }
    }
    
    return ""
}

Step 2: TDDサイクルでリファクタリング

🔴 Red Phase: 失敗するテストを作成

// cmd/hello/main_test.go
func TestHelloHandler_AuthenticationConsistency(t *testing.T) {
    t.Run("should_use_unified_token_extraction", func(t *testing.T) {
        // このテストは最初は失敗する(まだ古い実装のため)
        
        request := events.APIGatewayProxyRequest{
            HTTPMethod: "GET",
            Headers:    map[string]string{}, // Authorization header missing
        }
        
        response, err := businessHandler(context.Background(), request, log, metricsClient)
        
        require.NoError(t, err)
        // 統一されたエラーメッセージを期待
        assert.Equal(t, 401, response.StatusCode)
        
        var responseBody map[string]interface{}
        json.Unmarshal([]byte(response.Body), &responseBody)
        assert.Equal(t, "Authorization header missing", responseBody["error"])
    })
}

🟢 Green Phase: テストを通すための実装

✅ 改善後のコード(cmd/hello/main.go)

func businessHandler(ctx context.Context, request events.APIGatewayProxyRequest, log *logger.Logger, metricsClient *middleware.MetricsClient) (events.APIGatewayProxyResponse, error) {
    startTime := time.Now()

    // ✅ 統一された認証抽出関数を使用
    accessToken, err := utils.ExtractTokenFromRequest(request)
    if err != nil {
        var errorMessage string
        switch err {
        case utils.ErrAuthHeaderNotFound:
            errorMessage = "Authorization header missing"
        case utils.ErrEmptyAuthHeader:
            errorMessage = "Authorization header is empty"
        case utils.ErrInvalidAuthFormat:
            errorMessage = "Invalid authorization header format"
        case utils.ErrEmptyToken:
            errorMessage = "Empty token"
        default:
            errorMessage = "Authentication failed"
        }

        log.Warn("Authentication failed", map[string]interface{}{
            "error": err.Error(),
        })

        response := utils.CreateErrorResponse(401, "Unauthorized", errorMessage)
        
        // メトリクス記録
        if metricsClient != nil {
            metricsClient.RecordAPICall("hello-function", request.HTTPMethod, 401, time.Since(startTime))
        }

        return response, nil
    }
    
    // ✅ 46行の重複コードが3行に短縮!
    
    // ... 実際のビジネスロジック(Cognito認証等)
}

♻️ Refactor Phase: さらなる改善

同様のリファクタリングを他の関数にも適用:

  • cmd/update-user/main.go: 8行の重複コード削減
  • cmd/admin-users/main.go: 17行の重複コード削減
  • cmd/delete-user/main.go: 統一エラーハンドリング適用

Step 3: コンプライアンステストの実装

コンプライアンステストとは、コードが特定のルールや規約(この場合は統一された認証処理の使用)に従っているかを自動的にチェックするテストです。これにより、将来的に誰かが誤って手動の認証処理を再導入するのを防ぎ、コードの一貫性を維持できます。

// pkg/lambda/consistency_test.go
func TestLambdaFunctionCompliance(t *testing.T) {
    functions := []string{
        "hello", "update-user", "admin-users", "delete-user",
        "get-user", "change-password", // ... その他の関数
    }

    for _, functionName := range functions {
        t.Run(functionName, func(t *testing.T) {
            // ✅ 認証処理の一貫性をチェック
            assert.True(t, CheckUnifiedAuthenticationExtraction(functionName), 
                "Function %s should use utils.ExtractTokenFromRequest()", functionName)
                
            // ✅ 手動認証処理が残っていないかチェック
            assert.False(t, CheckManualAuthenticationParsing(functionName),
                "Function %s should not contain manual authentication parsing", functionName)
        })
    }
}

📊 結果:大幅な改善を達成

定量的な改善

項目改善前改善後削減量
cmd/hello/main.go46行の認証コード3行の統一関数呼び出し43行削減
cmd/update-user/main.go8行の重複コード統一パターン8行削減
cmd/admin-users/main.go17行の重複コード統一パターン17行削減
総削減コード行数--68行削減

定性的な改善

✅ 保守性の向上

// 🎉 修正が必要なのは1箇所だけ!
// utils.ExtractTokenFromRequest() を修正すれば、
// 全ての関数に反映される

✅ エラーハンドリングの統一

// 🎉 全関数で一貫したエラーメッセージ
"Authorization header missing"
"Invalid authorization header format"
"Empty token"

✅ テストの簡素化

// 🎉 認証テストは utils パッケージで一元化
// 各関数では統合テストに集中できる

✅ バグリスクの削減

// 🎉 実装差異によるバグがなくなった
// Case-insensitive処理も統一された

🧪 テストによる品質保証

改善前のテスト課題

  • 各関数で個別の認証テストが必要
  • テスト間での不整合
  • 重複したテストコード

改善後のテスト構造

// ✅ 統一認証のテスト
func TestExtractTokenFromRequest(t *testing.T) {
    tests := []struct {
        name        string
        headers     map[string]string
        expected    string
        expectedErr error
    }{
        {
            name:        "Valid Bearer token",
            headers:     map[string]string{"Authorization": "Bearer valid-token"},
            expected:    "valid-token",
            expectedErr: nil,
        },
        {
            name:        "Missing Authorization header",
            headers:     map[string]string{},
            expected:    "",
            expectedErr: ErrAuthHeaderNotFound,
        },
        // ... その他のテストケース
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            request := events.APIGatewayProxyRequest{Headers: tt.headers}
            token, err := ExtractTokenFromRequest(request)
            
            assert.Equal(t, tt.expected, token)
            assert.Equal(t, tt.expectedErr, err)
        })
    }
}

コンプライアンステスト

// ✅ 全Lambda関数の一貫性を自動チェック
func TestLambdaFunctionConsistency(t *testing.T) {
    functions := getAllLambdaFunctions()
    
    for _, funcName := range functions {
        t.Run(funcName, func(t *testing.T) {
            // 認証処理が統一されているかチェック
            assert.True(t, usesUnifiedAuth(funcName))
            // 手動パースが残っていないかチェック
            assert.False(t, hasManualAuthParsing(funcName))
        })
    }
}

🎓 学んだこと・初心者へのアドバイス

1. コードの重複は「技術的負債」

重複コードは最初は楽に書けますが、後から大きな負担になります:

  • ❌ 短期的: 早く実装できる
  • ❌ 長期的: 保守コストが exponential に増加

2. TDDの威力

TDDは初心者には難しく感じるかもしれませんが、実際は:

  • ✅ 安全なリファクタリング: テストがあるので恐れずに改善できる
  • ✅ 品質の担保: バグを早期発見できる
  • ✅ 設計の改善: テストしやすいコードは良い設計

3. 統一関数の設計ポイント

// 🎯 良い統一関数の特徴
func ExtractTokenFromRequest(request events.APIGatewayProxyRequest) (string, error) {
    // ✅ 明確な責務: トークンの抽出のみ
    // ✅ エラーの詳細化: 複数の error type で詳細な情報
    // ✅ テストしやすい: 純粋関数(副作用なし)
    // ✅ 再利用性: どのLambda関数からでも利用可能
}

4. 段階的な改善アプローチ

一度にすべてを変えるのではなく:

  1. 小さく始める: 1つの関数から
  2. テストで保護: リファクタリング前後でテスト
  3. 順次展開: 他の関数にも同じパターンを適用
  4. 継続的改善: コンプライアンステストで品質維持

🚀 今後の展望

今回の改善により、以下の基盤ができました:

新機能の追加が容易に

// 🎉 新しいLambda関数でも3行で認証実装
func newBusinessHandler(ctx context.Context, request events.APIGatewayProxyRequest, log *logger.Logger, metricsClient *middleware.MetricsClient) (events.APIGatewayProxyResponse, error) {
    // ✅ 統一認証(3行)
    token, err := utils.ExtractTokenFromRequest(request)
    if err != nil {
        return utils.CreateAuthErrorResponse(err), nil
    }
    
    // ✅ すぐにビジネスロジックに集中できる
    // ...
}

セキュリティ強化も一箇所で

// 🔒 将来的にJWT検証を追加する場合も
func ExtractTokenFromRequest(request events.APIGatewayProxyRequest) (string, error) {
    token, err := extractBearerToken(request)
    if err != nil {
        return "", err
    }
    
    // ✅ 一箇所でJWT検証を追加すれば全関数に適用
    if err := validateJWTFormat(token); err != nil {
        return "", ErrInvalidJWT
    }
    
    return token, nil
}

まとめ

認証処理の重複問題をTDDで解決した結果:

  • 📉 68行のコード削減 → 保守コスト大幅削減
  • 🔒 セキュリティの統一 → バグリスク削減
  • 🧪 テスト品質向上 → 信頼性の向上
  • 🚀 開発速度向上 → 新機能開発が高速化

参考リンク