golang-jwt
認証ライブラリ
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)))
}