Go の Gin での認証・認可

Web API において認証(Authentication)と認可(Authorization)は重要なセキュリティ要素だ。Gin では JWT を使った認証システムを比較的簡単に実装できる。

認証と認可の違い

認証(Authentication)

「誰であるか」を確認すること。ログイン処理でユーザーの身元を検証する。

認可(Authorization)

「何ができるか」を確認すること。認証済みユーザーが特定のリソースにアクセスできるか判定する。

JWT の基本

JWT(JSON Web Token)は、ユーザー情報を含んだトークンだ。サーバーはセッションを保持せずにユーザーを識別できる。

go get -u github.com/golang-jwt/jwt/v5

JWT の生成

package auth

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-secret-key")

type Claims struct {
    UserID uint   `json:"user_id"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func GenerateToken(userID uint, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

JWT の検証

func ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return jwtSecret, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, fmt.Errorf("invalid token")
}

ログインハンドラ

type LoginInput struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required"`
}

func Login(c *gin.Context) {
    var input LoginInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // ユーザー検索
    var user models.User
    if err := database.DB.Where("email = ?", input.Email).First(&user).Error; err != nil {
        c.JSON(401, gin.H{"error": "Invalid credentials"})
        return
    }
    
    // パスワード検証
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
        c.JSON(401, gin.H{"error": "Invalid credentials"})
        return
    }
    
    // トークン生成
    token, err := auth.GenerateToken(user.ID, user.Email, user.Role)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to generate token"})
        return
    }
    
    c.JSON(200, gin.H{"token": token})
}

認証ミドルウェア

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Authorization header required"})
            return
        }
        
        // "Bearer <token>" 形式を想定
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid authorization format"})
            return
        }
        
        claims, err := auth.ValidateToken(parts[1])
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid or expired token"})
            return
        }
        
        // コンテキストにユーザー情報を設定
        c.Set("userID", claims.UserID)
        c.Set("email", claims.Email)
        c.Set("role", claims.Role)
        c.Next()
    }
}

認可ミドルウェア(ロールベース)

特定のロールを持つユーザーだけがアクセスできるようにする。

func RoleRequired(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole, exists := c.Get("role")
        if !exists {
            c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
            return
        }
        
        roleStr := userRole.(string)
        for _, role := range roles {
            if roleStr == role {
                c.Next()
                return
            }
        }
        
        c.AbortWithStatusJSON(403, gin.H{"error": "Forbidden"})
    }
}

ルーティングへの適用

func main() {
    r := gin.Default()
    
    // 公開ルート
    r.POST("/login", handlers.Login)
    r.POST("/register", handlers.Register)
    
    // 認証が必要なルート
    authorized := r.Group("/")
    authorized.Use(AuthMiddleware())
    {
        authorized.GET("/profile", handlers.GetProfile)
        authorized.PUT("/profile", handlers.UpdateProfile)
        
        // 管理者専用ルート
        admin := authorized.Group("/admin")
        admin.Use(RoleRequired("admin"))
        {
            admin.GET("/users", handlers.AdminGetUsers)
            admin.DELETE("/users/:id", handlers.AdminDeleteUser)
        }
    }
    
    r.Run(":8080")
}

ヘルパー関数

ハンドラからユーザー情報を取得するヘルパーを用意すると便利だ。

func GetCurrentUserID(c *gin.Context) (uint, error) {
    userID, exists := c.Get("userID")
    if !exists {
        return 0, errors.New("user not found in context")
    }
    return userID.(uint), nil
}

// 使用例
func GetProfile(c *gin.Context) {
    userID, err := GetCurrentUserID(c)
    if err != nil {
        c.JSON(401, gin.H{"error": "Unauthorized"})
        return
    }
    
    var user models.User
    database.DB.First(&user, userID)
    c.JSON(200, user)
}

リフレッシュトークン

アクセストークンの有効期限を短くし、リフレッシュトークンで更新する方式もある。

アクセストークン

有効期限が短い(15分〜1時間)。API アクセスに使用する。

リフレッシュトークン

有効期限が長い(数日〜数週間)。アクセストークンの再発行に使用する。データベースに保存して管理する。

func RefreshToken(c *gin.Context) {
    refreshToken := c.PostForm("refresh_token")
    
    // リフレッシュトークンを検証(DBから取得して比較)
    var storedToken models.RefreshToken
    if err := database.DB.Where("token = ?", refreshToken).First(&storedToken).Error; err != nil {
        c.JSON(401, gin.H{"error": "Invalid refresh token"})
        return
    }
    
    // 新しいアクセストークンを発行
    newToken, _ := auth.GenerateToken(storedToken.UserID, storedToken.Email, storedToken.Role)
    
    c.JSON(200, gin.H{"token": newToken})
}

セキュリティのベストプラクティス

JWT シークレットは環境変数で管理し、ハードコードしない
HTTPS を必ず使用してトークンの盗聴を防ぐ
トークンの有効期限を適切に設定する
パスワードは bcrypt などでハッシュ化して保存する
ログアウト時はクライアント側でトークンを削除し、必要に応じてブラックリストを実装する

認証・認可は API セキュリティの基盤だ。要件に応じて OAuth2 や OpenID Connect などの標準プロトコルの採用も検討するとよい。