GoにおけるPASETOによるAPIセキュリティの強化
Takashi Yamamoto
Infrastructure Engineer · Leapcell

GoにおけるPASETOによるより安全なAPIの構築
Web APIセキュリティの状況は常に進化しています。長年にわたり、JSON Web Tokens(JWT)は、当事者間で情報をコンパクトかつ自己完結型で転送する方法を提供し、主要な力となってきました。しかし、その普及が進むにつれて、特に実装の複雑さ、アルゴリズムの柔軟性、および不適切な検証に関連するリスクについて、潜在的な欠陥への認識も高まりました。開発者として、私たちはアプリケーションを保護するための、より堅牢で開発者に優しいソリューションを常に求めています。この追求は、セキュリティを簡素化しながら全体的な回復力を高める代替手段を探求する多くの人々を導いてきました。このような有望な代替手段の1つである、プラットフォーム非依存セキュリティトークン(PASETO)は、大きな注目を集めています。
この記事では、GoにおけるPASETOを使用したAPI認証の実装について詳しく説明し、従来のJWTと比較して、より安全で簡単なアプローチをどのように提供できるかを示します。
セキュアなトークンの柱を理解する
実践的な実装に入る前に、議論の基礎となる重要な概念の基本的な理解を確立しましょう。これらの多くはJWTの経験から馴染みがあるかもしれませんが、PASETOエコシステム内での具体的なニュアンスを把握することが重要です。
PASETO(Platform-Agnostic Security Tokens): 基本的に、PASETOはJWTの安全な代替手段です。JWTとは異なり、PASETOはデフォルトで安全であり、暗号化とトークン構造のベストプラクティスを組み込んでいるため、一般的なセキュリティ設定ミスが発生する可能性を減らします。常に署名(または暗号化および署名)されており、改ざんを防ぎ、認証を保証します。PASETOには、local
(暗号化)とpublic
(署名)の2つの主要な種類があります。サーバーはクライアントのIDを確認する必要があり、トークンの内容をクライアントから非公開にする必要はないため、API認証には主にpublic
トークンに焦点を当てます。
対称鍵暗号化: PASETO local
トークンで使用され、同じ鍵が暗号化と復号化の両方に使用されます。これは、トークン発行者とコンシューマーが同じエンティティであるか、機密鍵を安全に共有している場合に適しています。
非対称鍵暗号化: PASETO public
トークン(および私たちが使用するもの)で使用され、数学的にリンクされた鍵のペア:公開鍵と秘密鍵が含まれます。秘密鍵はトークンに署名するために使用され、公開鍵は署名を検証するために使用されます。これは、発行者がトークンに署名し、複数のコンシューマー(公開鍵のみを所有している)がそれらを偽造できないようにしながら検証する必要があるシナリオに理想的です。これは、サーバーがトークンに署名し、クライアントアプリケーションがそれらを検証する(暗黙的に、サーバーに返送して検証してもらう)API認証に完璧にマッピングされます。
クレーム: JWTとPASETOトークンの両方には、トークンのサブジェクトに関する表明を表す「クレーム」のセットであるペイロードが含まれています。これらは通常JSONオブジェクトで、iss
(発行者)、sub
(サブジェクト)、exp
(有効期限)、およびカスタムアプリケーション固有のクレームなどの標準クレームを含めることができます。
フッター: PASETOのユニークな機能は、オプションのフッターです。これにより、任意の、暗号化も署名もされていないデータをトークンに追加できます。フッターに機密データを格納することは推奨されませんが、暗号化整合性チェックの一部である必要のないコンテキスト情報、たとえば鍵IDなどに便利です。
GoでのAPI認証のためのPASETOの実装
API認証にPASETOを使用する中心的な考え方はシンプルです。ユーザーが正常に認証されたとき(たとえば、正しいユーザー名とパスワードを提供したとき)、サーバーはユーザーのID情報を含むPASETO public
トークンを発行します。このトークンはクライアントに返送されます。後続のAPIリクエストでは、クライアントはこのPASETOをAuthorization
ヘッダーに含めます。サーバーは、公開鍵を使用してPASETOの署名を検証し、クレームからユーザーのIDを抽出してリクエストを承認します。
実践的なGo実装を見てみましょう。
まず、Go用の堅牢なPASETOライブラリが必要です。@o1eglによるpaseto
パッケージは、人気があり、よくメンテナンスされている選択肢です。インストールします。
go get github.com/o1egl/paseto
次に、認証システムの構造を検討します。キーペアの生成、トークンの発行、トークンの検証を行う関数が必要になります。
1. 非対称鍵の生成
public
PASETOトークンには、Ed25519非対称鍵ペアが必要です。秘密鍵を安全に保存し、公開鍵を検証用に配布することが重要です。デモンストレーション目的で、メモリ内で生成します。本番環境では、これらは安全なストレージからロードされます。
package main import ( "crypto/rand" "fmt" "golang.org/x/crypto/ed25519" ) // generateKeyPair generates an Ed25519 public/private key pair. func generateKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, nil, fmt.Errorf("failed to generate Ed25519 key pair: %w", err) } return publicKey, privateKey, nil } // In a real application, you would load these from environment variables or a key management system. // For demonstration, let's keep them global. var ( appPrivateKey ed25519.PrivateKey appPublicKey ed25519.PublicKey ) func init() { var err error appPublicKey, appPrivateKey, err = generateKeyPair() if err != nil { panic(fmt.Sprintf("failed to initialize application keys: %v", err)) } fmt.Println("Keys generated successfully.") }
2. PASETOトークンの発行
ユーザーがログインすると、トークンを作成します。このトークンにはUserEmail
やUserID
などのクレームが含まれ、有効期限が設定されます。
package main import ( "fmt" "time" "github.com/o1egl/paseto" ) // UserClaims defines the structure for our token claims. type UserClaims struct { paseto.JSONToken UserEmail string `json:"user_email"` UserID string `json:"user_id"` } // issueToken creates a new PASETO public token. func issueToken(userID, userEmail string, duration time.Duration) (string, error) { // Create a new PASETO V2 public builder v2 := paseto.NewV2() // Prepare claims now := time.Now() exp := now.Add(duration) claims := UserClaims{ JSONToken: paseto.JSONToken{ IssuedAt: now, Expiration: exp, NotBefore: now, }, UserID: userID, UserEmail: userEmail, } // Sign the token with the private key token, err := v2.Sign(appPrivateKey, claims, "some-optional-footer") // Footer is optional if err != nil { return "", fmt.Errorf("failed to sign PASETO token: %w", err) } return token, nil }
3. PASETOトークンの検証とクレームの抽出
保護されたAPI呼び出しごとに、サーバーはトークンを受信し、公開鍵を使用してその有効性を検証してから、クレームを抽出します。
package main import ( "fmt" "time" "github.com/o1egl/paseto" ) // verifyToken verifies a PASETO public token and extracts its claims. func verifyToken(token string) (*UserClaims, error) { v2 := paseto.NewV2() claims := &UserClaims{} footer := "" // If you used a footer, you'd specify it here. // Verify the token with the public key err := v2.Verify(token, appPublicKey, claims, footer) if err != nil { return nil, fmt.Errorf("failed to verify PASETO token: %w", err) } // PASETO library automatically checks expiration and nbf by default. // You can add additional checks if needed, e.g., for custom claims. return claims, nil }
4. APIエンドポイントへの統合(Goのnet/http
を使用した例)
フローをデモンストレーションするために、簡単なHTTPサーバーをセットアップしましょう。
package main import ( "encoding/json" "fmt" "log" "net/http" "time" ) // Login request body type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } // Login response body type LoginResponse struct { Token string `json:"token"` } // Authenticate simulates a login process and issues a token. func Authenticate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // In a real app, validate credentials against a database if req.Username != "testuser" || req.Password != "password123" { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } // Issue a PASETO token token, err := issueToken("user-123", req.Username+"@example.com", 24*time.Hour) if err != nil { log.Printf("Error issuing token: %v", err) http.Error(w, "Failed to issue token", http.StatusInternalServerError) return } resp := LoginResponse{Token: token} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // AuthMiddleware is a middleware to protect API endpoints. func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "Authorization header required", http.StatusUnauthorized) return } // Expecting "Bearer <PASETO_TOKEN>" if len(authHeader) < 7 || authHeader[:7] != "Bearer " { http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized) return } pasetoToken := authHeader[7:] claims, err := verifyToken(pasetoToken) if err != nil { log.Printf("PASETO verification failed: %v", err) http.Error(w, "Invalid or expired token", http.StatusUnauthorized) return } // Token is valid, you can now use claims.UserID and claims.UserEmail // to identify the user and perform authorization checks. // For example, store user info in request context for downstream handlers. log.Printf("User %s (%s) authenticated successfully.", claims.UserID, claims.UserEmail) // Proceed to the next handler next.ServeHTTP(w, r) } } // ProtectedEndpoint is an example of an API that requires authentication. func ProtectedEndpoint(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Welcome to the protected area!") } func main() { http.HandleFunc("/login", Authenticate) http.HandleFunc("/protected", AuthMiddleware(ProtectedEndpoint)) fmt.Println("Server listening on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
この例を実行するには:
- コードを
main.go
として保存します。 - (まだ初期化されていない場合は)
go mod init myapp
を実行します。 go mod tidy
を実行します。go run main.go
を実行します。
次にcurl
でテストできます。:
1. トークンを取得するためにログインする:
curl -X POST -H "Content-Type: application/json" -d '{"username": "testuser", "password": "password123"}' http://localhost:8080/login
これにより、PASETOトークンを含むJSONオブジェクトが返されます。トークンをコピーしてください。
2. トークンを使用して保護されたエンドポイントにアクセスする:
(<YOUR_PASETO_TOKEN>
を、受け取ったトークンに置き換えてください)
curl -H "Authorization: Bearer <YOUR_PASETO_TOKEN>" http://localhost:8080/protected
「Welcome to the protected area!」と表示されるはずです。トークンを省略したり、無効なトークンを送信したりすると、Unauthorized
エラーが表示されます。
JWTよりもPASETOである理由?
paseto
ライブラリは、設計上、JWT実装でオプションであり、しばしば見過ごされるいくつかのセキュリティベストプラクティスを強制します。:
- デフォルトで安全: PASETOはバージョン(例:V1、V2、V3、V4)を明示的に定義しており、それぞれが特定の堅牢な暗号アルゴリズム(例:V2は公開鍵にEd25519、ローカルトークンにXChacha20-Poly1305を使用)に結び付けられています。これにより、
none
のような安全でないアルゴリズムが誤って使用される可能性のある、JWTでよく見られる「アルゴリズムの柔軟性」問題が排除されます。 - 改ざん防止: すべてのPASETOトークンは、暗号化署名(公開トークン)または暗号化および署名(ローカルトークン)されています。トークンを無効にすることなく、簡単にデコードおよび再エンコードできる生ペイロードはありません。
- シンプルさと予測可能性: PASETO仕様はJWTよりも簡潔で曖昧さが少ないため、実装エラーが少なく、セキュリティ保証が明確になります。
- 暗号アジリティの脆弱性がない: PASETOのバージョン管理は、攻撃者が検証者に弱いアルゴリズムの使用を強制する(例:RSAから公開鍵を秘密鍵として使用するHMACに切り替える)ことを完全に防ぎます。
アプリケーションシナリオ
PASETOは、さまざまなAPI認証シナリオに最適です。:
- マイクロサービス間通信: ユーザーコンテキストや認証データをサービス間で安全に転送します。
- Web API認証: デモンストレーションされた主なユースケース。クライアントはトークンを取得し、後続のリクエストの認証に使用します。
- サーバー間認証:
local
PASETOトークンは、共通の共通鍵を共有する信頼できるサービス間の安全な通信に使用できます。 - パスワードレス認証: PASETOトークンは、電子メールまたはSMS経由で送信される安全で有効期限付きのログイントークンとして機能します。
結論
PASETOは、認証済みトークンに対する新鮮でセキュリティ・ファーストのアプローチを提供し、暗号化の堅牢性と開発者の利便性の間に良いバランスを取っています。Go APIプロジェクトでPASETOを採用することにより、トークンベースの認証によく関連付けられる攻撃対象領域を大幅に削減し、より回復力があり信頼性の高いアプリケーションを構築できます。ベストプラクティスを組み込んだその意図的な設計は、特に「デフォルトで安全」という原則を優先する開発者にとって、JWTの魅力的な代替手段となります。提供されたコード例は、GoでのPASETOの実装が簡単であることを示しており、成熟した暗号プリミティブとよく設計されたライブラリを活用してAPI通信を保護しています。
GoアプリケーションにPASETOを選択することは、本質的に安全で保守性の高いシステムを構築する一歩であり、現代のセキュリティ課題を念頭に置いて設計されたトークン標準を採用することです。