Echo JWT

認証ライブラリGoEchoJWTミドルウェアセキュリティ

認証ライブラリ

Echo JWT

概要

Echo JWTは、GoのEchoフレームワーク専用のJWT認証ミドルウェアです。2025年現在、CVE-2024-51744のセキュリティ脆弱性を受けてEchoコアから分離され、独立したパッケージとして提供されています。golang-jwt/jwt/v5を内部実装に使用し、Echoアプリケーションに安全で効率的なJWT認証機能を提供します。Echo v4との互換性を保ちながら、最新のセキュリティ要件に対応した実装となっています。

詳細

Echo JWTは、Echoフレームワークとシームレスに統合されるJWT認証ミドルウェアです。主な特徴:

  • Echoネイティブ統合: Echo v4との完全な互換性と最適化された統合
  • JWT v5対応: 最新のgolang-jwt/jwt/v5を使用したセキュアな実装
  • 自動検証: Authorizationヘッダーからのトークン自動抽出と検証
  • 柔軟な設定: 署名キー、アルゴリズム、抽出方法のカスタマイズ対応
  • エラーハンドリング: 不正なトークンに対する適切なHTTPレスポンス
  • クレーム取得: 検証済みJWTクレームへの簡単アクセス

メリット・デメリット

メリット

  • EchoフレームワークとのネイティブかつEfficient な統合
  • セキュリティ脆弱性に対する迅速な対応と更新
  • 軽量で高パフォーマンスなJWT認証機能
  • シンプルなAPIで使いやすい設定とカスタマイズ
  • Goらしい慣用的なコードスタイル
  • Echo v4との完全互換性を維持

デメリット

  • Echoフレームワーク専用で他のフレームワークでは使用不可
  • 独立パッケージのため依存関係が増加
  • golang-jwt/jwtバージョン間の型アサーション問題の可能性
  • 機能が基本的なJWT認証に限定される
  • 複雑な認証フローには追加実装が必要

参考ページ

書き方の例

基本的なインストールとセットアップ

# Echo JWT ミドルウェアのインストール
go get github.com/labstack/echo-jwt/v4
go get github.com/golang-jwt/jwt/v5

# Echo v4 用のパッケージ
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware

基本的なJWT認証設定

package main

import (
    "net/http"
    "time"

    "github.com/golang-jwt/jwt/v5"
    echojwt "github.com/labstack/echo-jwt/v4"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type jwtCustomClaims struct {
    UserID   int    `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

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

func main() {
    e := echo.New()

    // 基本ミドルウェア
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())

    // パブリックルート
    e.POST("/login", login)
    e.POST("/register", register)
    e.GET("/public", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "message": "パブリックエンドポイント",
        })
    })

    // JWT保護されたルートグループ
    protected := e.Group("/api")
    protected.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))

    protected.GET("/profile", getProfile)
    protected.PUT("/profile", updateProfile)
    protected.GET("/admin", adminOnly)

    e.Logger.Fatal(e.Start(":8080"))
}

func login(c echo.Context) error {
    type LoginRequest struct {
        Username string `json:"username" validate:"required"`
        Password string `json:"password" validate:"required"`
    }

    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "不正なリクエストフォーマット",
        })
    }

    // ユーザー認証(実際の実装では適切な認証ロジックを使用)
    if req.Username != "admin" || req.Password != "password" {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "認証に失敗しました",
        })
    }

    // JWTトークン生成
    claims := &jwtCustomClaims{
        UserID:   1,
        Username: req.Username,
        Role:     "admin",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
            Subject:   "user-authentication",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(jwtSecret)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "トークン生成に失敗しました",
        })
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "message": "ログイン成功",
        "token":   tokenString,
        "user": map[string]interface{}{
            "id":       claims.UserID,
            "username": claims.Username,
            "role":     claims.Role,
        },
    })
}

プロテクトされたエンドポイントの実装

func getProfile(c echo.Context) error {
    // JWTトークンからユーザー情報を取得
    token, ok := c.Get("user").(*jwt.Token)
    if !ok {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "JWTトークンが見つかりません",
        })
    }

    claims, ok := token.Claims.(*jwtCustomClaims)
    if !ok {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "無効なクレーム形式",
        })
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "user_id":  claims.UserID,
        "username": claims.Username,
        "role":     claims.Role,
        "issued_at": claims.IssuedAt,
        "expires_at": claims.ExpiresAt,
    })
}

func updateProfile(c echo.Context) error {
    token := c.Get("user").(*jwt.Token)
    claims := token.Claims.(*jwtCustomClaims)

    type UpdateProfileRequest struct {
        Email    string `json:"email"`
        FullName string `json:"full_name"`
    }

    var req UpdateProfileRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "不正なリクエストフォーマット",
        })
    }

    // プロファイル更新ロジック(データベース操作など)
    // userService.UpdateProfile(claims.UserID, req)

    return c.JSON(http.StatusOK, map[string]interface{}{
        "message": "プロファイルが更新されました",
        "user_id": claims.UserID,
        "updated_fields": req,
    })
}

ロールベースアクセス制御(RBAC)

// カスタムミドルウェア:ロール確認
func requireRole(allowedRoles ...string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            token := c.Get("user").(*jwt.Token)
            claims := token.Claims.(*jwtCustomClaims)

            for _, role := range allowedRoles {
                if claims.Role == role {
                    return next(c)
                }
            }

            return c.JSON(http.StatusForbidden, map[string]string{
                "error": "このリソースへのアクセス権限がありません",
            })
        }
    }
}

func adminOnly(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "message": "管理者のみアクセス可能なエンドポイント",
    })
}

// ルートグループでの使用
func setupRoutes(e *echo.Echo) {
    // 管理者専用ルート
    admin := e.Group("/admin")
    admin.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))
    admin.Use(requireRole("admin"))
    admin.GET("/users", adminOnly)
    admin.DELETE("/users/:id", deleteUser)

    // 通常ユーザー用ルート
    user := e.Group("/user")
    user.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: jwtSecret,
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },
    }))
    user.Use(requireRole("user", "admin"))
    user.GET("/dashboard", userDashboard)
}

高度な設定とカスタマイズ

func advancedJWTConfig() echojwt.Config {
    return echojwt.Config{
        // 署名キー(環境変数から取得推奨)
        SigningKey: []byte("your-secret-key"),

        // 署名アルゴリズムの指定
        SigningMethod: "HS256",

        // クレームの作成関数
        NewClaimsFunc: func(c echo.Context) jwt.Claims {
            return new(jwtCustomClaims)
        },

        // トークン抽出方法のカスタマイズ
        TokenLookup: "header:Authorization:Bearer ,cookie:token,query:token",

        // 認証失敗時のエラーハンドラー
        ErrorHandler: func(c echo.Context, err error) error {
            return c.JSON(http.StatusUnauthorized, map[string]interface{}{
                "error": "認証に失敗しました",
                "details": err.Error(),
                "timestamp": time.Now().Unix(),
            })
        },

        // Beforeフック:トークン検証前の処理
        BeforeFunc: func(c echo.Context) {
            // リクエストログやメトリクス収集など
            c.Logger().Info("JWT認証を開始します")
        },

        // Successフック:認証成功後の処理
        SuccessHandler: func(c echo.Context) {
            token := c.Get("user").(*jwt.Token)
            claims := token.Claims.(*jwtCustomClaims)
            c.Logger().Infof("ユーザー %s が認証されました", claims.Username)
        },

        // キースト機能(複数の署名キー対応)
        KeyFunc: func(t *jwt.Token) (interface{}, error) {
            // トークンのヘッダーに基づいて適切なキーを返す
            if keyID, ok := t.Header["kid"].(string); ok {
                return getKeyByID(keyID), nil
            }
            return jwtSecret, nil
        },
    }
}

func getKeyByID(keyID string) []byte {
    // キーIDに基づいて適切な署名キーを返す
    keys := map[string][]byte{
        "key1": []byte("secret-key-1"),
        "key2": []byte("secret-key-2"),
    }
    
    if key, exists := keys[keyID]; exists {
        return key
    }
    return jwtSecret // デフォルトキー
}

リフレッシュトークンの実装

type RefreshTokenClaims struct {
    UserID int    `json:"user_id"`
    Type   string `json:"type"` // "refresh"
    jwt.RegisteredClaims
}

func generateTokenPair(userID int, username, role string) (string, string, error) {
    // アクセストークン(短期間)
    accessClaims := &jwtCustomClaims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 15)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
        },
    }

    accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    accessTokenString, err := accessToken.SignedString(jwtSecret)
    if err != nil {
        return "", "", err
    }

    // リフレッシュトークン(長期間)
    refreshClaims := &RefreshTokenClaims{
        UserID: userID,
        Type:   "refresh",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)), // 1週間
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "echo-jwt-app",
        },
    }

    refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refreshTokenString, err := refreshToken.SignedString(jwtSecret)
    if err != nil {
        return "", "", err
    }

    return accessTokenString, refreshTokenString, nil
}

func refreshToken(c echo.Context) error {
    type RefreshRequest struct {
        RefreshToken string `json:"refresh_token"`
    }

    var req RefreshRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "不正なリクエストフォーマット",
        })
    }

    // リフレッシュトークンの検証
    token, err := jwt.ParseWithClaims(req.RefreshToken, &RefreshTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })

    if err != nil || !token.Valid {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "無効なリフレッシュトークン",
        })
    }

    claims := token.Claims.(*RefreshTokenClaims)
    if claims.Type != "refresh" {
        return c.JSON(http.StatusUnauthorized, map[string]string{
            "error": "無効なトークンタイプ",
        })
    }

    // 新しいアクセストークンを生成
    // ユーザー情報を取得(データベースから)
    user := getUserByID(claims.UserID) // 実装が必要

    accessToken, _, err := generateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "トークン生成に失敗しました",
        })
    }

    return c.JSON(http.StatusOK, map[string]string{
        "access_token": accessToken,
        "token_type":   "Bearer",
    })
}