Echo JWT
認証ライブラリ
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",
})
}