golang-jwt

認証ライブラリGoJWTJSON Web Tokenセキュリティトークン認証デジタル署名

認証ライブラリ

golang-jwt

概要

golang-jwtは、Go言語用の純粋JWT(JSON Web Token)実装ライブラリで、RFC 7519標準に完全準拠したJWTのエンコード、デコード、検証機能を提供します。HMAC、RSA、ECDSA、EdDSA等の多様な署名アルゴリズムをサポートし、カスタムクレームの柔軟な扱いや高度なセキュリティ機能を備えています。シンプルで直感的なAPIでありながら、パフォーマンスとセキュリティの両面で優れた設計を持ち、マイクロサービス、API認証、シングルサインオンなどの幅広いユースケースで使用されています2025年現在も活発に開発が続けられ、Go言語のJWTエコシステムで最も信頼性の高いライブラリの一つです。

詳細

golang-jwtは、JWTトークンの完全なライフサイクル管理を提供するGoライブラリです。以下の主な特徴があります:

  • RFC 7519準拠: JWT標準仕様に完全準拠した安全で信頼性の高い実装
  • 多様な署名アルゴリズム: HMAC (HS256/384/512)、RSA (RS256/384/512)、ECDSA (ES256/384/512)、EdDSAをサポート
  • カスタムクレーム: 柔軟なクレーム処理とバリデーション機能
  • セキュリティ機能: トークン有効期限、Issuer/Audience検証、カスタムバリデーション
  • パフォーマンス: 高速なパーシングとメモリ効率の良い設計
  • 拡張性: カスタムキー関数、トークンパーサー、バリデーターの実装対応

メリット・デメリット

メリット

  • JWT標準への完全準拠により他のシステムとの高い互換性を実現
  • 豊富な署名アルゴリズムサポートで幅広いセキュリティ要件に対応
  • シンプルなAPIでありながら高度なカスタマイズ性を提供
  • 高速な処理性能とメモリ効率の優秀性
  • 軽量で依存関係が少なく、マイクロサービスに適した設計
  • 活発な開発コミュニティと豊富なドキュメント・サンプル

デメリット

  • 純粋JWTライブラリのため高レベルな認証機能は含まれていない
  • JWTセキュリティの理解と適切な実装が開発者に求められる
  • Go以外の言語環境では使用できない
  • トークンの永続化やセッション管理機能は別途実装が必要

参考ページ

書き方の例

基本的なJWTトークン生成と検証

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// カスタムクレーム構造体
type CustomClaims struct {
	UserID   string `json:"user_id"`
	Username string `json:"username"`
	Role     string `json:"role"`
	jwt.RegisteredClaims
}

var secretKey = []byte("your-256-bit-secret")

func main() {
	// JWTトークンの生成
	token, err := generateJWT("user-123", "johndoe", "admin")
	if err != nil {
		log.Fatal("Failed to generate token:", err)
	}
	fmt.Println("生成されたJWTトークン:", token)

	// JWTトークンの検証
	claims, err := validateJWT(token)
	if err != nil {
		log.Fatal("Failed to validate token:", err)
	}
	fmt.Printf("検証成功 - User ID: %s, Username: %s, Role: %s\n",
		claims.UserID, claims.Username, claims.Role)
}

// JWTトークン生成関数
func generateJWT(userID, username, role string) (string, error) {
	// カスタムクレームの作成
	claims := CustomClaims{
		UserID:   userID,
		Username: username,
		Role:     role,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "your-app",
			Subject:   userID,
			Audience:  []string{"your-api"},
			ID:        fmt.Sprintf("jwt-%d", time.Now().Unix()),
		},
	}

	// トークン作成
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// 署名して文字列として返す
	tokenString, err := token.SignedString(secretKey)
	if err != nil {
		return "", fmt.Errorf("failed to sign token: %w", err)
	}

	return tokenString, nil
}

// JWTトークン検証関数
func validateJWT(tokenString string) (*CustomClaims, error) {
	// トークンのパーシングと検証
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, 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 secretKey, nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to parse token: %w", err)
	}

	// クレームの取得と検証
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, fmt.Errorf("invalid token")
}

RSA公開鍵暗号でのJWT処理

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

var (
	privateKey *rsa.PrivateKey
	publicKey  *rsa.PublicKey
)

func init() {
	// RSA鍵ペアの生成(実際にはファイルから読み込み)
	var err error
	privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		log.Fatal("秘密鍵生成エラー:", err)
	}
	publicKey = &privateKey.PublicKey
}

func main() {
	// RSAで署名したJWTトークン生成
	token, err := generateRSAJWT("user-456", "alice", "user")
	if err != nil {
		log.Fatal("RSA JWT生成エラー:", err)
	}
	fmt.Println("RSA JWTトークン:", token)

	// RSA公開鍵での検証
	claims, err := validateRSAJWT(token)
	if err != nil {
		log.Fatal("RSA JWT検証エラー:", err)
	}
	fmt.Printf("RSA JWT検証成功 - User: %s, Role: %s\n",
		claims.Username, claims.Role)
}

// RSA秘密鍵でJWT生成
func generateRSAJWT(userID, username, role string) (string, error) {
	claims := CustomClaims{
		UserID:   userID,
		Username: username,
		Role:     role,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "rsa-issuer",
			Subject:   userID,
			Audience:  []string{"rsa-api"},
		},
	}

	// RSA256で署名
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
	tokenString, err := token.SignedString(privateKey)
	if err != nil {
		return "", fmt.Errorf("failed to sign RSA token: %w", err)
	}

	return tokenString, nil
}

// RSA公開鍵でJWT検証
func validateRSAJWT(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		// RSA署名メソッドの検証
		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return publicKey, nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to parse RSA token: %w", err)
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, fmt.Errorf("invalid RSA token")
}

// PEMフォーマットの鍵の読み込み例
func loadRSAKeysFromFile() {
	// 秘密鍵の読み込み
	privateKeyPEM := `-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----`

	block, _ := pem.Decode([]byte(privateKeyPEM))
	if block == nil || block.Type != "RSA PRIVATE KEY" {
		log.Fatal("Failed to decode PEM block containing private key")
	}

	parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		log.Fatal("秘密鍵パーシングエラー:", err)
	}
	privateKey = parsedKey

	// 公開鍵の読み込み
	publicKeyPEM := `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`

	block, _ = pem.Decode([]byte(publicKeyPEM))
	if block == nil || block.Type != "PUBLIC KEY" {
		log.Fatal("Failed to decode PEM block containing public key")
	}

	parsedPubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		log.Fatal("公開鍵パーシングエラー:", err)
	}

	publicKey = parsedPubKey.(*rsa.PublicKey)
}

JWTミドルウェアの実装

package main

import (
	"context"
	"net/http"
	"strings"

	"github.com/golang-jwt/jwt/v5"
)

// JWTミドルウェア関数
func jwtMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Authorizationヘッダーからトークンを抽出
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" {
			http.Error(w, "Authorization header required", http.StatusUnauthorized)
			return
		}

		// Bearerトークンのフォーマット検証
		parts := strings.Split(authHeader, " ")
		if len(parts) != 2 || parts[0] != "Bearer" {
			http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
			return
		}

		tokenString := parts[1]

		// JWTトークンの検証
		claims, err := validateJWT(tokenString)
		if err != nil {
			http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
			return
		}

		// コンテキストにユーザー情報を設定
		ctx := context.WithValue(r.Context(), "user", claims)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// ロールベースのアクセス制御ミドルウェア
func requireRole(role string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			user := r.Context().Value("user").(*CustomClaims)
			if user.Role != role {
				http.Error(w, "Insufficient permissions", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

// スコープベースのアクセス制御ミドルウェア
func requireScope(requiredScope string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			user := r.Context().Value("user").(*CustomClaims)
			
			// スコープ情報をカスタムクレームから取得(例)
			if scopes, ok := user.RegisteredClaims.Subject.(string); ok {
				if !strings.Contains(scopes, requiredScope) {
					http.Error(w, "Insufficient scope", http.StatusForbidden)
					return
				}
			}
			
			next.ServeHTTP(w, r)
		})
	}
}

// HTTPサーバーのセットアップ例
func setupServer() {
	mux := http.NewServeMux()

	// パブリックエンドポイント
	mux.HandleFunc("/login", loginHandler)
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Welcome to JWT API"))
	})

	// 認証が必要なエンドポイント
	protected := http.NewServeMux()
	protected.HandleFunc("/profile", profileHandler)
	protected.HandleFunc("/data", dataHandler)

	// 管理者のみアクセス可能なエンドポイント
	admin := http.NewServeMux()
	admin.HandleFunc("/admin/users", adminUsersHandler)
	admin.HandleFunc("/admin/settings", adminSettingsHandler)

	// ミドルウェアの適用
	mux.Handle("/api/", http.StripPrefix("/api", jwtMiddleware(protected)))
	mux.Handle("/admin/", requireRole("admin")(jwtMiddleware(admin)))

	log.Println("Server starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

// ハンドラー関数の例
func loginHandler(w http.ResponseWriter, r *http.Request) {
	// 簡単なログイン処理
	username := r.FormValue("username")
	password := r.FormValue("password")

	if username == "admin" && password == "password" {
		token, err := generateJWT("admin-123", "admin", "admin")
		if err != nil {
			http.Error(w, "Token generation failed", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(fmt.Sprintf(`{"token": "%s", "expires_in": 86400}`, token)))
	} else {
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
	}
}

func profileHandler(w http.ResponseWriter, r *http.Request) {
	user := r.Context().Value("user").(*CustomClaims)
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(fmt.Sprintf(`{
		"user_id": "%s",
		"username": "%s",
		"role": "%s",
		"expires_at": "%s"
	}`, user.UserID, user.Username, user.Role, user.ExpiresAt.Time)))
}

func adminUsersHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(`{"users": ["user1", "user2", "user3"]}`))
}

カスタムバリデーションとクレーム処理

package main

import (
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// 拡張クレーム構造体
type ExtendedClaims struct {
	UserID      string                 `json:"user_id"`
	Username    string                 `json:"username"`
	Role        string                 `json:"role"`
	Permissions []string               `json:"permissions"`
	Metadata    map[string]interface{} `json:"metadata"`
	jwt.RegisteredClaims
}

// カスタムバリデーションメソッド
func (c ExtendedClaims) Valid() error {
	// 標準バリデーションを実行
	if err := c.RegisteredClaims.Valid(); err != nil {
		return err
	}

	// カスタムバリデーションルール
	if c.UserID == "" {
		return fmt.Errorf("user_id is required")
	}

	if c.Username == "" {
		return fmt.Errorf("username is required")
	}

	validRoles := []string{"admin", "user", "guest"}
	roleValid := false
	for _, validRole := range validRoles {
		if c.Role == validRole {
			roleValid = true
			break
		}
	}
	if !roleValid {
		return fmt.Errorf("invalid role: %s", c.Role)
	}

	// 権限の検証
	if len(c.Permissions) == 0 {
		return fmt.Errorf("at least one permission is required")
	}

	return nil
}

// 拡張JWT生成関数
func generateExtendedJWT(userID, username, role string, permissions []string, metadata map[string]interface{}) (string, error) {
	claims := ExtendedClaims{
		UserID:      userID,
		Username:    username,
		Role:        role,
		Permissions: permissions,
		Metadata:    metadata,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(4 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "extended-jwt-service",
			Subject:   userID,
			Audience:  []string{"api-service", "web-app"},
			ID:        fmt.Sprintf("ext-jwt-%d", time.Now().UnixNano()),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(secretKey)
}

// 拡張JWT検証関数
func validateExtendedJWT(tokenString string) (*ExtendedClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &ExtendedClaims{}, 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 secretKey, nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to parse extended token: %w", err)
	}

	if claims, ok := token.Claims.(*ExtendedClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, fmt.Errorf("invalid extended token")
}

// 権限チェックヘルパー関数
func hasPermission(claims *ExtendedClaims, requiredPermission string) bool {
	for _, permission := range claims.Permissions {
		if permission == requiredPermission {
			return true
		}
	}
	return false
}

// 使用例
func extendedJWTExample() {
	// メタデータの作成
	metadata := map[string]interface{}{
		"department":    "engineering",
		"employee_id":   "EMP-12345",
		"last_login":    time.Now().Format(time.RFC3339),
		"login_count":   42,
		"is_verified":   true,
		"preferences": map[string]interface{}{
			"theme":    "dark",
			"language": "ja",
		},
	}

	// 権限リスト
	permissions := []string{"read:profile", "write:profile", "read:documents", "create:reports"}

	// 拡張JWT生成
	token, err := generateExtendedJWT("emp-12345", "john.doe", "user", permissions, metadata)
	if err != nil {
		log.Fatal("拡張JWT生成エラー:", err)
	}
	fmt.Println("拡張JWTトークン:", token)

	// 拡張JWT検証
	claims, err := validateExtendedJWT(token)
	if err != nil {
		log.Fatal("拡張JWT検証エラー:", err)
	}

	fmt.Printf("検証成功 - User: %s, Department: %s\n",
		claims.Username, claims.Metadata["department"])

	// 権限チェック
	if hasPermission(claims, "create:reports") {
		fmt.Println("レポート作成権限あり")
	} else {
		fmt.Println("レポート作成権限なし")
	}
}

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

package main

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// リフレッシュトークン用のクレーム
type RefreshClaims struct {
	UserID    string `json:"user_id"`
	TokenType string `json:"token_type"`
	jwt.RegisteredClaims
}

// トークンペア構造体
type TokenPair struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
}

// アクセストークンとリフレッシュトークンのペア生成
func generateTokenPair(userID, username, role string) (*TokenPair, error) {
	accessTokenExpiry := time.Now().Add(15 * time.Minute) // 短い有効期限
	refreshTokenExpiry := time.Now().Add(7 * 24 * time.Hour) // 長い有効期限

	// アクセストークンの生成
	accessClaims := CustomClaims{
		UserID:   userID,
		Username: username,
		Role:     role,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(accessTokenExpiry),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "token-service",
			Subject:   userID,
			Audience:  []string{"api"},
		},
	}

	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
	accessTokenString, err := accessToken.SignedString(secretKey)
	if err != nil {
		return nil, fmt.Errorf("failed to generate access token: %w", err)
	}

	// リフレッシュトークンの生成
	refreshID, err := generateRandomID()
	if err != nil {
		return nil, fmt.Errorf("failed to generate refresh token ID: %w", err)
	}

	refreshClaims := RefreshClaims{
		UserID:    userID,
		TokenType: "refresh",
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(refreshTokenExpiry),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
			Issuer:    "token-service",
			Subject:   userID,
			ID:        refreshID,
		},
	}

	refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
	refreshTokenString, err := refreshToken.SignedString(secretKey)
	if err != nil {
		return nil, fmt.Errorf("failed to generate refresh token: %w", err)
	}

	return &TokenPair{
		AccessToken:  accessTokenString,
		RefreshToken: refreshTokenString,
		TokenType:    "Bearer",
		ExpiresIn:    int64(accessTokenExpiry.Sub(time.Now()).Seconds()),
	}, nil
}

// リフレッシュトークンを使用したアクセストークンの更新
func refreshAccessToken(refreshTokenString string) (*TokenPair, error) {
	// リフレッシュトークンの検証
	token, err := jwt.ParseWithClaims(refreshTokenString, &RefreshClaims{}, 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 secretKey, nil
	})

	if err != nil {
		return nil, fmt.Errorf("invalid refresh token: %w", err)
	}

	refreshClaims, ok := token.Claims.(*RefreshClaims)
	if !ok || !token.Valid {
		return nil, fmt.Errorf("invalid refresh token claims")
	}

	if refreshClaims.TokenType != "refresh" {
		return nil, fmt.Errorf("not a refresh token")
	}

	// リフレッシュトークンのBlacklistチェック(実際の実装ではデータベースやRedisを使用)
	if isTokenBlacklisted(refreshClaims.ID) {
		return nil, fmt.Errorf("refresh token has been revoked")
	}

	// ユーザー情報の取得(実際にはデータベースから)
	userInfo := getUserInfo(refreshClaims.UserID)
	if userInfo == nil {
		return nil, fmt.Errorf("user not found")
	}

	// 新しいトークンペアの生成
	newTokenPair, err := generateTokenPair(userInfo.UserID, userInfo.Username, userInfo.Role)
	if err != nil {
		return nil, fmt.Errorf("failed to generate new token pair: %w", err)
	}

	// 古いリフレッシュトークンをBlacklistに追加
	addToBlacklist(refreshClaims.ID)

	return newTokenPair, nil
}

// ランダムID生成
func generateRandomID() (string, error) {
	bytes := make([]byte, 16)
	_, err := rand.Read(bytes)
	if err != nil {
		return "", err
	}
	return hex.EncodeToString(bytes), nil
}

// ユーザー情報構造体(例)
type UserInfo struct {
	UserID   string
	Username string
	Role     string
	Active   bool
}

// ユーザー情報取得(ダミー実装)
func getUserInfo(userID string) *UserInfo {
	users := map[string]*UserInfo{
		"user-123": {UserID: "user-123", Username: "johndoe", Role: "user", Active: true},
		"admin-456": {UserID: "admin-456", Username: "admin", Role: "admin", Active: true},
	}
	return users[userID]
}

// Blacklist管理(ダミー実装)
var blacklist = make(map[string]bool)

func isTokenBlacklisted(tokenID string) bool {
	return blacklist[tokenID]
}

func addToBlacklist(tokenID string) {
	blacklist[tokenID] = true
}

// トークンリフレッシュのHTTPハンドラー
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	refreshToken := r.FormValue("refresh_token")
	if refreshToken == "" {
		http.Error(w, "Refresh token required", http.StatusBadRequest)
		return
	}

	newTokenPair, err := refreshAccessToken(refreshToken)
	if err != nil {
		http.Error(w, "Token refresh failed: "+err.Error(), http.StatusUnauthorized)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(fmt.Sprintf(`{
		"access_token": "%s",
		"refresh_token": "%s",
		"token_type": "%s",
		"expires_in": %d
	}`, newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.TokenType, newTokenPair.ExpiresIn)))
}