認証処理の重複を解決!
はじめに
今回は、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にも同様のコード
何が悪かったのか?
- コードの重複: 同じ処理を複数箇所で実装しているため、コード量が増え、見通しが悪くなります。
- 保守性の低下: 認証ロジックに修正が必要になった場合、すべてのLambda関数を個別に変更する必要があり、手間がかかります。
- バグの温床: 各関数で個別に実装しているため、実装に差異が生まれやすく、それがバグの原因となるリスクが高まります。
- テストの複雑化: 各関数で認証部分のテストを個別に書く必要があり、テストコードの量も増え、管理が大変になります。
🎯 解決方針:TDDで統一的な認証処理に改善
TDDとは?
TDD(Test-Driven Development:テスト駆動開発) は以下のサイクルで開発を進める手法です:
- Red: 最初に失敗するテストを書きます。まだ実装されていない機能に対するテストなので、当然失敗します。
- Green: そのテストを通すための最小限の実装を行います。余計な機能は追加せず、テストが成功することだけを目指します。
- Refactor: テストが通ったことを確認したら、コードを改善(リファクタリング)します。機能は変えずに、コードの可読性や保守性を高めます。
このサイクルを繰り返すことで、高品質なコードを効率的に開発できます。
改善計画
- 統一認証関数の作成: 認証処理を一箇所にまとめた
utils.ExtractTokenFromRequest()関数を作成します。 - 各Lambda関数のリファクタリング: 各Lambda関数から重複している認証コードを削除し、作成した統一認証関数に置き換えます。
- テストでの品質確保: 統一認証関数に対してテストを徹底的に行い、一貫性のあるエラーハンドリングが実現されていることを確認します。
🔧 実装:ステップバイステップで改善
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.go | 46行の認証コード | 3行の統一関数呼び出し | 43行削減 |
| cmd/update-user/main.go | 8行の重複コード | 統一パターン | 8行削減 |
| cmd/admin-users/main.go | 17行の重複コード | 統一パターン | 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つの関数から
- テストで保護: リファクタリング前後でテスト
- 順次展開: 他の関数にも同じパターンを適用
- 継続的改善: コンプライアンステストで品質維持
🚀 今後の展望
今回の改善により、以下の基盤ができました:
新機能の追加が容易に
// 🎉 新しい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行のコード削減 → 保守コスト大幅削減
- 🔒 セキュリティの統一 → バグリスク削減
- 🧪 テスト品質向上 → 信頼性の向上
- 🚀 開発速度向上 → 新機能開発が高速化
参考リンク
- TDD(Test-Driven Development)とは?
- Go言語でのAWS Lambda開発
- 認証・認可のベストプラクティス yamadatt@ubuntu2204:/media/tv_record/poc-cognite$


