go-guardian

認証ライブラリGo多要素認証セキュリティJWTBasic認証API認証

認証ライブラリ

go-guardian

概要

go-guardianは、Go言語用の包括的な認証・認可ライブラリです。複数の認証戦略(JWT、Basic認証、API Key、OTP等)を統一したインターフェースで提供し、マルチファクター認証やカスタム認証戦略にも対応しています。シンプルなAPIでありながら高度なセキュリティ機能を実装でき、エンタープライズレベルのアプリケーションに適した柔軟性と拡張性を持っています。フレームワークに依存しない設計のため、net/http、Gin、Echo等の様々なGoウェブフレームワークで利用可能で、2025年現在も活発に開発が続けられている信頼性の高いライブラリです。

詳細

go-guardianは、認証と認可を統一的に扱うためのGoライブラリです。以下の主な特徴があります:

  • 多様な認証戦略: JWT、Basic認証、Bearer Token、API Key、OTP、X.509証明書認証をサポート
  • マルチファクター認証: TOTP、HOTP、SMS認証等の2FA/MFA実装
  • カスタム戦略対応: 独自の認証メカニズムを簡単に実装・統合可能
  • フレームワーク非依存: 標準のnet/httpから各種フレームワークまで幅広く対応
  • キャッシュ機能: Redis、メモリキャッシュによる認証結果の高速化
  • ユーザー情報管理: 柔軟なユーザーデータ構造とパーミッション管理

メリット・デメリット

メリット

  • 複数の認証方式を統一的なAPIで管理でき、実装が簡潔
  • エンタープライズレベルのセキュリティ要件に対応可能
  • フレームワークに依存しない柔軟な設計で移植性が高い
  • カスタム認証戦略の実装が容易で拡張性に優れる
  • 多要素認証の標準サポートでセキュリティを強化
  • キャッシュ機能による高いパフォーマンス

デメリット

  • 多機能なため初期学習コストが高い
  • 設定項目が多く、適切な設定に時間を要する
  • Go以外の言語では使用できない
  • 単純な認証要件には過剰な機能セット

参考ページ

書き方の例

基本的なセットアップとBasic認証

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/shaj13/go-guardian/v2/auth"
	"github.com/shaj13/go-guardian/v2/auth/strategies/basic"
	"github.com/shaj13/go-guardian/v2/auth/strategies/bearer"
)

// ユーザー情報の定義
type User struct {
	ID       string
	UserName string
	Email    string
	Roles    []string
}

// auth.Infoインターフェースの実装
func (u User) GetID() string {
	return u.ID
}

func (u User) GetUserName() string {
	return u.UserName
}

func main() {
	keeper := auth.New()
	cache := auth.NewFIFO(context.Background(), time.Minute*5)

	// Basic認証戦略の設定
	basicStrategy := basic.New(validateUser, cache)
	keeper.EnableStrategy(basic.StrategyKey, basicStrategy)

	// Bearer Token認証戦略の設定
	bearerStrategy := bearer.New(validateToken, cache)
	keeper.EnableStrategy(bearer.CachedStrategyKey, bearerStrategy)

	// HTTP ハンドラーの設定
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		user, err := keeper.Authenticate(r)
		if err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		fmt.Fprintf(w, "Hello %s, ID: %s", user.GetUserName(), user.GetID())
	})

	http.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
		user, err := keeper.Authenticate(r)
		if err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// ロールベースのアクセス制御
		if u, ok := user.(User); ok {
			if !hasRole(u.Roles, "admin") {
				http.Error(w, "Forbidden", http.StatusForbidden)
				return
			}
		}

		fmt.Fprintf(w, "Admin area for %s", user.GetUserName())
	})

	http.ListenAndServe(":8080", nil)
}

// Basic認証のユーザー検証
func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
	// 実際の実装では、データベースから認証情報を取得
	users := map[string]string{
		"admin": "admin123",
		"user":  "user123",
	}

	if validPassword, exists := users[userName]; exists && validPassword == password {
		return User{
			ID:       userName,
			UserName: userName,
			Email:    userName + "@example.com",
			Roles:    getRoles(userName),
		}, nil
	}

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

// Bearer Token検証
func validateToken(ctx context.Context, r *http.Request, token string) (auth.Info, error) {
	// 簡単なトークン検証(実際にはJWT検証等を実装)
	validTokens := map[string]User{
		"admin-token": {
			ID:       "admin",
			UserName: "admin",
			Email:    "[email protected]",
			Roles:    []string{"admin", "user"},
		},
		"user-token": {
			ID:       "user",
			UserName: "user",
			Email:    "[email protected]",
			Roles:    []string{"user"},
		},
	}

	if user, exists := validTokens[token]; exists {
		return user, nil
	}

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

func hasRole(roles []string, targetRole string) bool {
	for _, role := range roles {
		if role == targetRole {
			return true
		}
	}
	return false
}

func getRoles(userName string) []string {
	if userName == "admin" {
		return []string{"admin", "user"}
	}
	return []string{"user"}
}

JWT認証戦略の実装

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/shaj13/go-guardian/v2/auth"
	"github.com/shaj13/go-guardian/v2/auth/strategies/jwt"
)

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

type CustomClaims struct {
	UserID string   `json:"user_id"`
	Roles  []string `json:"roles"`
	jwt.StandardClaims
}

func setupJWTAuth() auth.Strategy {
	// JWTキー関数
	keyfunc := 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
	}

	// カスタムクレーム解析
	parser := jwt.NewParser(jwt.NewWithClaims(jwt.SigningMethodHS256, &CustomClaims{}))

	// JWT戦略の作成
	strategy := jwt.New(cache, keyfunc)
	strategy.SetParser(jwt.SetExpectedIssuer("your-app"), jwt.SetExpectedAudience("your-api"))

	return strategy
}

// JWTトークンの生成
func generateJWT(userID string, roles []string) (string, error) {
	claims := CustomClaims{
		UserID: userID,
		Roles:  roles,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
			Issuer:    "your-app",
			Audience:  "your-api",
			IssuedAt:  time.Now().Unix(),
		},
	}

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

// JWT認証のユーザー情報解析
func parseJWTUser(ctx context.Context, r *http.Request, token *jwt.Token) (auth.Info, error) {
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return User{
			ID:       claims.UserID,
			UserName: claims.UserID,
			Email:    claims.UserID + "@example.com",
			Roles:    claims.Roles,
		}, nil
	}
	return nil, fmt.Errorf("invalid token")
}

// ログインエンドポイント
func loginHandler(w http.ResponseWriter, r *http.Request) {
	username := r.FormValue("username")
	password := r.FormValue("password")

	// ユーザー認証(簡単な例)
	if username == "admin" && password == "admin123" {
		token, err := generateJWT("admin", []string{"admin", "user"})
		if err != nil {
			http.Error(w, "Failed to generate token", http.StatusInternalServerError)
			return
		}

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

多要素認証(MFA)の実装

package main

import (
	"context"
	"crypto/rand"
	"encoding/base32"
	"fmt"
	"time"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
	"github.com/shaj13/go-guardian/v2/auth"
	"github.com/shaj13/go-guardian/v2/auth/strategies/twofactor"
)

type MFAUser struct {
	ID        string
	UserName  string
	Email     string
	Roles     []string
	TOTPSecret string
	MFAEnabled bool
}

func (u MFAUser) GetID() string {
	return u.ID
}

func (u MFAUser) GetUserName() string {
	return u.UserName
}

// TOTP設定の生成
func generateTOTP(userID string) (*otp.Key, error) {
	return totp.Generate(totp.GenerateOpts{
		Issuer:      "YourApp",
		AccountName: userID,
		Period:      30,
		Digits:      6,
		Algorithm:   otp.AlgorithmSHA1,
	})
}

// TOTP検証
func validateTOTP(secret, token string) bool {
	return totp.Validate(token, secret)
}

// 2段階認証戦略の設定
func setup2FA() auth.Strategy {
	// プライマリ認証(Basic認証)
	primary := basic.New(validateUserForMFA, cache)

	// セカンダリ認証(TOTP)
	secondary := twofactor.New(validateMFAToken, cache)

	// 2段階認証戦略
	return twofactor.NewStrategy(primary, secondary)
}

// MFA対応ユーザー検証
func validateUserForMFA(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
	// データベースからユーザー情報を取得(簡単な例)
	users := map[string]MFAUser{
		"admin": {
			ID:         "admin",
			UserName:   "admin",
			Email:      "[email protected]",
			Roles:      []string{"admin", "user"},
			TOTPSecret: "JBSWY3DPEHPK3PXP", // 実際には暗号化して保存
			MFAEnabled: true,
		},
	}

	if user, exists := users[userName]; exists {
		// パスワード検証(実際にはハッシュ化して比較)
		if password == "admin123" {
			return user, nil
		}
	}

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

// MFAトークン検証
func validateMFAToken(ctx context.Context, r *http.Request, user auth.Info, token string) (auth.Info, error) {
	mfaUser, ok := user.(MFAUser)
	if !ok {
		return nil, fmt.Errorf("invalid user type")
	}

	if !mfaUser.MFAEnabled {
		return user, nil // MFA無効の場合はそのまま通す
	}

	if validateTOTP(mfaUser.TOTPSecret, token) {
		return user, nil
	}

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

// MFA設定エンドポイント
func setupMFAHandler(w http.ResponseWriter, r *http.Request) {
	userID := r.FormValue("user_id")
	if userID == "" {
		http.Error(w, "User ID required", http.StatusBadRequest)
		return
	}

	key, err := generateTOTP(userID)
	if err != nil {
		http.Error(w, "Failed to generate TOTP", http.StatusInternalServerError)
		return
	}

	// QRコード用のURLを生成
	qrURL := key.URL()

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{
	"secret": "%s",
	"qr_url": "%s",
	"manual_entry_key": "%s"
}`, key.Secret(), qrURL, key.Secret())
}

カスタム認証戦略の実装

package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strconv"
	"time"

	"github.com/shaj13/go-guardian/v2/auth"
)

// API Key認証戦略
type APIKeyStrategy struct {
	cache auth.Cache
}

func NewAPIKeyStrategy(cache auth.Cache) *APIKeyStrategy {
	return &APIKeyStrategy{cache: cache}
}

// auth.Strategyインターフェースの実装
func (a *APIKeyStrategy) Authenticate(ctx context.Context, r *http.Request) (auth.Info, error) {
	apiKey := r.Header.Get("X-API-Key")
	timestamp := r.Header.Get("X-Timestamp")
	signature := r.Header.Get("X-Signature")

	if apiKey == "" || timestamp == "" || signature == "" {
		return nil, fmt.Errorf("missing required headers")
	}

	// キャッシュから確認
	if info, ok := a.cache.Load(apiKey); ok {
		return info, nil
	}

	// タイムスタンプ検証(5分以内)
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid timestamp")
	}

	if time.Now().Unix()-ts > 300 {
		return nil, fmt.Errorf("request too old")
	}

	// 署名検証
	if !a.validateSignature(apiKey, timestamp, signature, r) {
		return nil, fmt.Errorf("invalid signature")
	}

	// API Key からユーザー情報を取得
	user, err := a.getUserByAPIKey(apiKey)
	if err != nil {
		return nil, err
	}

	// キャッシュに保存
	a.cache.Store(apiKey, user, time.Minute*5)

	return user, nil
}

func (a *APIKeyStrategy) validateSignature(apiKey, timestamp, signature string, r *http.Request) bool {
	// 秘密鍵を取得(実際にはデータベースから)
	secret := a.getSecretByAPIKey(apiKey)
	if secret == "" {
		return false
	}

	// 署名対象文字列の作成
	method := r.Method
	path := r.URL.Path
	query := r.URL.RawQuery
	signString := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, path, query, timestamp, apiKey)

	// HMAC-SHA256で署名を生成
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(signString))
	expectedSignature := hex.EncodeToString(h.Sum(nil))

	return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func (a *APIKeyStrategy) getUserByAPIKey(apiKey string) (auth.Info, error) {
	// データベースからAPI Keyに対応するユーザーを取得
	apiKeys := map[string]User{
		"api-key-123": {
			ID:       "api-user-1",
			UserName: "api-user",
			Email:    "[email protected]",
			Roles:    []string{"api"},
		},
	}

	if user, exists := apiKeys[apiKey]; exists {
		return user, nil
	}

	return nil, fmt.Errorf("invalid API key")
}

func (a *APIKeyStrategy) getSecretByAPIKey(apiKey string) string {
	// API Keyに対応する秘密鍵を取得
	secrets := map[string]string{
		"api-key-123": "secret-key-456",
	}

	return secrets[apiKey]
}

// カスタム戦略の使用例
func useCustomStrategy() {
	keeper := auth.New()
	cache := auth.NewFIFO(context.Background(), time.Minute*10)

	// カスタムAPI Key戦略を登録
	apiKeyStrategy := NewAPIKeyStrategy(cache)
	keeper.EnableStrategy("api-key", apiKeyStrategy)

	// 他の戦略と組み合わせ
	basicStrategy := basic.New(validateUser, cache)
	keeper.EnableStrategy(basic.StrategyKey, basicStrategy)

	http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
		user, err := keeper.Authenticate(r)
		if err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		fmt.Fprintf(w, "Data accessed by: %s", user.GetUserName())
	})
}

Gin フレームワークとの統合

package main

import (
	"context"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/shaj13/go-guardian/v2/auth"
	"github.com/shaj13/go-guardian/v2/auth/strategies/basic"
)

// Gin ミドルウェア
func authMiddleware(keeper auth.Authenticator) gin.HandlerFunc {
	return func(c *gin.Context) {
		user, err := keeper.Authenticate(c.Request)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"error": "Authentication failed",
				"message": err.Error(),
			})
			c.Abort()
			return
		}

		// ユーザー情報をコンテキストに保存
		c.Set("user", user)
		c.Next()
	}
}

// ロール制限ミドルウェア
func requireRole(role string) gin.HandlerFunc {
	return func(c *gin.Context) {
		user, exists := c.Get("user")
		if !exists {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found in context"})
			c.Abort()
			return
		}

		if u, ok := user.(User); ok {
			if !hasRole(u.Roles, role) {
				c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
				c.Abort()
				return
			}
		}

		c.Next()
	}
}

func setupGinApp() {
	r := gin.Default()

	// 認証設定
	keeper := auth.New()
	cache := auth.NewFIFO(context.Background(), time.Minute*5)
	basicStrategy := basic.New(validateUser, cache)
	keeper.EnableStrategy(basic.StrategyKey, basicStrategy)

	// パブリックエンドポイント
	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "Welcome to the API"})
	})

	// 認証が必要なエンドポイント
	auth := r.Group("/auth")
	auth.Use(authMiddleware(keeper))
	{
		auth.GET("/profile", func(c *gin.Context) {
			user, _ := c.Get("user")
			c.JSON(http.StatusOK, gin.H{"user": user})
		})

		// 管理者のみアクセス可能
		admin := auth.Group("/admin")
		admin.Use(requireRole("admin"))
		{
			admin.GET("/users", func(c *gin.Context) {
				c.JSON(http.StatusOK, gin.H{"users": []string{"user1", "user2"}})
			})
		}
	}

	r.Run(":8080")
}