Gin API を RBAC と JWT 検証で保護する
このガイドでは、Logto が発行する ロールベースのアクセス制御 (RBAC) と JSON Web Token (JWT) を使用して Gin API に認可 (Authorization) を実装し、セキュリティを強化する方法を説明します。
始める前に
クライアントアプリケーションは Logto から アクセス トークン (Access token) を取得する必要があります。まだクライアント統合を設定していない場合は、React、Vue、Angular などのクライアントフレームワーク向け クイックスタート や、サーバー間アクセス用の マシン間通信 (M2M) ガイド をご覧ください。
このガイドは、Gin アプリケーションにおけるこれらのトークンの サーバーサイド検証 に焦点を当てています。

学べること
- JWT 検証:アクセス トークン (Access token) を検証し、認証 (Authentication) 情報を抽出する方法
- ミドルウェア実装:API 保護のための再利用可能なミドルウェアの作成
- 権限モデル:さまざまな認可 (Authorization) パターンの理解と実装
- アプリケーション全体のエンドポイント向けグローバル API リソース
- テナント固有の機能制御のための組織 (Organization) 権限
- マルチテナントデータアクセスのための組織レベル API リソース
- RBAC 統合:API エンドポイントでロールベースの権限 (Permission) とスコープ (Scope) を強制する方法
前提条件
- Go の最新安定版がインストールされていること
- Gin および Web API 開発の基礎知識
- Logto アプリケーションが設定済み(必要に応じて クイックスタート を参照)
権限モデルの概要
保護を実装する前に、アプリケーションアーキテクチャに適した権限モデルを選択してください。これは Logto の 3 つの主要な 認可 (Authorization) シナリオ に対応しています:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース

- ユースケース: アプリケーション全体で共有される API リソースを保護する(組織固有ではない)
- トークンタイプ: グローバルオーディエンスを持つアクセス トークン
- 例: パブリック API、コアプロダクトサービス、管理エンドポイント
- 最適: すべての顧客が利用する API を持つ SaaS プロダクト、テナント分離のないマイクロサービス
- 詳細: グローバル API リソースの保護

- ユースケース: 組織固有のアクション、UI 機能、ビジネスロジックを制御する(API ではない)
- トークンタイプ: 組織固有オーディエンスを持つ組織トークン
- 例: 機能ゲーティング、ダッシュボード権限、メンバー招待コントロール
- 最適: 組織固有の機能やワークフローを持つマルチテナント SaaS
- 詳細: 組織(非 API)権限の保護

- ユースケース: 特定の組織コンテキスト内でアクセス可能な API リソースを保護する
- トークンタイプ: API リソースオーディエンス + 組織コンテキストを持つ組織トークン
- 例: マルチテナント API、組織スコープのデータエンドポイント、テナント固有のマイクロサービス
- 最適: API データが組織スコープとなるマルチテナント SaaS
- 詳細: 組織レベル API リソースの保護
💡 進める前にモデルを選択してください — このガイド全体で選択したアプローチを参照します。
クイック準備手順
Logto リソースと権限の設定
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
- API リソースの作成: コンソール → API リソース にアクセスし、API を登録します(例:
https://api.yourapp.com
) - 権限の定義:
read:products
やwrite:orders
などのスコープを追加します – 権限付き API リソースの定義 を参照 - グローバルロールの作成: コンソール → ロール にアクセスし、API 権限を含むロールを作成します – グローバルロールの設定 を参照
- ロールの割り当て: API アクセスが必要なユーザーまたは M2M アプリケーションにロールを割り当てます
- 組織権限の定義: 組織テンプレートで
invite:member
やmanage:billing
などの非 API 組織権限を作成します - 組織ロールの設定: 組織テンプレートで組織固有のロールを設定し、それらに権限を割り当てます
- 組織ロールの割り当て: 各組織コンテキスト内でユーザーに組織ロールを割り当てます
- API リソースの作成: 上記と同様に API リソースを登録しますが、組織コンテキストで使用します
- 権限の定義: 組織コンテキストにスコープされた
read:data
やwrite:settings
などのスコープを追加します - 組織テンプレートの設定: API リソース権限を含む組織ロールを設定します
- 組織ロールの割り当て: API 権限を含む組織ロールにユーザーまたは M2M アプリケーションを割り当てます
- マルチテナント設定: API が組織スコープのデータとバリデーションを処理できることを確認します
ロールベースのアクセス制御ガイド からステップバイステップのセットアップ手順を始めましょう。
クライアントアプリケーションの更新
クライアントで適切なスコープをリクエストする:
- ユーザー認証 (Authentication): アプリの更新 → で API スコープや組織コンテキストをリクエスト
- マシン間通信: M2M スコープの設定 → でサーバー間アクセスを設定
通常、クライアント設定を次のいずれか、または複数を含めるように更新します:
- OAuth フローでの
scope
パラメーター - API リソースアクセス用の
resource
パラメーター - 組織コンテキスト用の
organization_id
テストするユーザーまたは M2M アプリが、API に必要な権限を含む適切なロールまたは組織ロールに割り当てられていることを確認してください。
API プロジェクトの初期化
Gin を使って新しい Go プロジェクトを初期化するには、次の手順を実行します:
go mod init your-api-name
go get github.com/gin-gonic/gin
次に、基本的な Gin サーバーのセットアップを作成します:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Run(":3000") // 0.0.0.0:3000 でリッスンおよびサーブ
}
ルート、ミドルウェア、その他の機能の設定方法については Gin のドキュメントを参照してください。
定数とユーティリティの初期化
トークンの抽出と検証を処理するために、コード内で必要な定数やユーティリティを定義してください。有効なリクエストには、Authorization
ヘッダーが Bearer <アクセス トークン (Access token)>
の形式で含まれている必要があります。
package main
import (
"fmt"
"net/http"
"strings"
)
const (
JWKS_URI = "https://your-tenant.logto.app/oidc/jwks"
ISSUER = "https://your-tenant.logto.app/oidc"
)
type AuthorizationError struct {
Message string
Status int
}
func (e *AuthorizationError) Error() string {
return e.Message
}
func NewAuthorizationError(message string, status ...int) *AuthorizationError {
statusCode := http.StatusForbidden // デフォルトは 403 Forbidden
if len(status) > 0 {
statusCode = status[0]
}
return &AuthorizationError{
Message: message,
Status: statusCode,
}
}
func extractBearerTokenFromHeaders(r *http.Request) (string, error) {
const bearerPrefix = "Bearer "
authorization := r.Header.Get("Authorization")
if authorization == "" {
return "", NewAuthorizationError("Authorization ヘッダーがありません", http.StatusUnauthorized)
}
if !strings.HasPrefix(authorization, bearerPrefix) {
return "", NewAuthorizationError(fmt.Sprintf("Authorization ヘッダーは \"%s\" で始まる必要があります", bearerPrefix), http.StatusUnauthorized)
}
return strings.TrimPrefix(authorization, bearerPrefix), nil
}
Logto テナント情報の取得
Logto が発行したトークンを検証するには、次の値が必要です:
- JSON Web Key Set (JWKS) URI:JWT 署名を検証するために使用される Logto の公開鍵の URL。
- 発行者 (Issuer):期待される発行者値(Logto の OIDC URL)。
まず、Logto テナントのエンドポイントを見つけます。これはさまざまな場所で確認できます:
- Logto コンソールの 設定 → ドメイン で確認できます。
- Logto で設定した任意のアプリケーション設定内の 設定 → エンドポイント & 資格情報 で確認できます。
OpenID Connect ディスカバリーエンドポイントから取得する
これらの値は、Logto の OpenID Connect ディスカバリーエンドポイントから取得できます:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
以下はレスポンス例です(他のフィールドは省略しています):
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
コード内にハードコーディングする(推奨されません)
Logto では JWKS URI や発行者 (Issuer) のカスタマイズができないため、これらの値をコード内にハードコーディングすることも可能です。ただし、将来的に設定が変更された場合のメンテナンス負荷が増加する可能性があるため、本番アプリケーションでは推奨されません。
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- 発行者 (Issuer):
https://<your-logto-endpoint>/oidc
トークンと権限の検証
トークンを抽出し OIDC 設定を取得した後、次の点を検証してください:
- 署名:JWT は有効であり、Logto(JWKS 経由)によって署名されている必要があります。
- 発行者 (Issuer):Logto テナントの発行者 (Issuer) と一致している必要があります。
- オーディエンス (Audience):Logto に登録された API のリソースインジケーター、または該当する場合は組織コンテキストと一致している必要があります。
- 有効期限:トークンが有効期限切れでないこと。
- 権限 (スコープ) (Permissions (scopes)):トークンに API / アクションに必要なスコープが含まれている必要があります。スコープは
scope
クレーム内のスペース区切り文字列です。 - 組織コンテキスト:組織レベルの API リソースを保護する場合、
organization_id
クレームを検証してください。
JWT の構造やクレームについて詳しくは JSON Web Token を参照してください。
各権限モデルで確認すべきこと
クレームや検証ルールは権限モデルによって異なります:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
- オーディエンスクレーム (
aud
): API リソースインジケーター - 組織クレーム (
organization_id
): なし - チェックするスコープ(権限) (
scope
): API リソース権限
- オーディエンスクレーム (
aud
):urn:logto:organization:<id>
(組織コンテキストがaud
クレームに含まれる) - 組織クレーム (
organization_id
): なし - チェックするスコープ(権限) (
scope
): 組織権限
- オーディエンスクレーム (
aud
): API リソースインジケーター - 組織クレーム (
organization_id
): 組織 ID(リクエストと一致する必要あり) - チェックするスコープ(権限) (
scope
): API リソース権限
非 API 組織権限の場合、組織コンテキストは aud
クレーム(例:urn:logto:organization:abc123
)で表されます。organization_id
クレームは組織レベル API リソーストークンにのみ存在します。
セキュアなマルチテナント API のため、必ず権限(スコープ)とコンテキスト(オーディエンス、組織)の両方を検証してください。
検証ロジックの追加
github.com/lestrrat-go/jwx を使用して JWT の検証を行います。まだインストールしていない場合は、インストールしてください:
go mod init your-project
go get github.com/lestrrat-go/jwx/v3
まず、これらの共通コンポーネントを auth_middleware.go
に追加します:
import (
"context"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var jwkSet jwk.Set
func init() {
// JWKS キャッシュの初期化
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
jwkSet, err = jwk.Fetch(ctx, JWKS_URI)
if err != nil {
panic("Failed to fetch JWKS: " + err.Error())
}
}
// validateJWT は JWT を検証し、パース済みトークンを返します
func validateJWT(tokenString string) (jwt.Token, error) {
token, err := jwt.Parse([]byte(tokenString), jwt.WithKeySet(jwkSet))
if err != nil {
return nil, NewAuthorizationError("Invalid token: "+err.Error(), http.StatusUnauthorized)
}
// 発行者 (Issuer) の検証
if token.Issuer() != ISSUER {
return nil, NewAuthorizationError("Invalid issuer", http.StatusUnauthorized)
}
if err := verifyPayload(token); err != nil {
return nil, err
}
return token, nil
}
// トークンデータを抽出するヘルパー関数
func getStringClaim(token jwt.Token, key string) string {
if val, ok := token.Get(key); ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getScopesFromToken(token jwt.Token) []string {
if val, ok := token.Get("scope"); ok {
if scope, ok := val.(string); ok && scope != "" {
return strings.Split(scope, " ")
}
}
return []string{}
}
func getAudienceFromToken(token jwt.Token) []string {
return token.Audience()
}
次に、アクセストークンの検証用ミドルウェアを実装します:
import "github.com/gin-gonic/gin"
func VerifyAccessToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := extractBearerTokenFromHeaders(c.Request)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
token, err := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
// トークンをコンテキストに保存し、汎用的に利用できるようにする
c.Set("auth", token)
c.Next()
}
}
権限モデルに応じて、異なる verifyPayload
ロジックを採用する必要があります:
- グローバル API リソース
- 組織(非 API)権限
- 組織レベル API リソース
func verifyPayload(token jwt.Token) error {
// audience クレームが API リソースインジケーターと一致するか確認
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Invalid audience")
}
// グローバル API リソースに必要なスコープを確認
requiredScopes := []string{"api:read", "api:write"} // 実際に必要なスコープに置き換えてください
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient scope")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// audience クレームが組織フォーマットと一致するか確認
if !hasOrganizationAudience(token) {
return NewAuthorizationError("Invalid audience for organization permissions")
}
// 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出する必要があります)
expectedOrgID := "your-organization-id" // リクエストコンテキストから抽出
if !hasMatchingOrganization(token, expectedOrgID) {
return NewAuthorizationError("Organization ID mismatch")
}
// 必要な組織スコープを確認
requiredScopes := []string{"invite:users", "manage:settings"} // 実際に必要なスコープに置き換えてください
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient organization scope")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// audience クレームが API リソースインジケーターと一致するか確認
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Invalid audience for organization-level API resources")
}
// 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出する必要があります)
expectedOrgID := "your-organization-id" // リクエストコンテキストから抽出
if !hasMatchingOrganizationID(token, expectedOrgID) {
return NewAuthorizationError("Organization ID mismatch")
}
// 組織レベル API リソースに必要なスコープを確認
requiredScopes := []string{"api:read", "api:write"} // 実際に必要なスコープに置き換えてください
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient organization-level API scopes")
}
return nil
}
ペイロード検証用のヘルパー関数を追加します:
// hasAudience はトークンに指定された audience が含まれているか確認します
func hasAudience(token jwt.Token, expectedAud string) bool {
audiences := token.Audience()
for _, aud := range audiences {
if aud == expectedAud {
return true
}
}
return false
}
// hasOrganizationAudience はトークンに組織 audience フォーマットが含まれているか確認します
func hasOrganizationAudience(token jwt.Token) bool {
audiences := token.Audience()
for _, aud := range audiences {
if strings.HasPrefix(aud, "urn:logto:organization:") {
return true
}
}
return false
}
// hasRequiredScopes はトークンにすべての必要なスコープが含まれているか確認します
func hasRequiredScopes(token jwt.Token, requiredScopes []string) bool {
scopes := getScopesFromToken(token)
for _, required := range requiredScopes {
found := false
for _, scope := range scopes {
if scope == required {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// hasMatchingOrganization はトークンの audience が期待される組織と一致するか確認します
func hasMatchingOrganization(token jwt.Token, expectedOrgID string) bool {
expectedAud := fmt.Sprintf("urn:logto:organization:%s", expectedOrgID)
return hasAudience(token, expectedAud)
}
// hasMatchingOrganizationID はトークンの organization_id が期待されるものと一致するか確認します
func hasMatchingOrganizationID(token jwt.Token, expectedOrgID string) bool {
orgID := getStringClaim(token, "organization_id")
return orgID == expectedOrgID
}
ミドルウェアを API に適用する
これで、保護された API ルートにミドルウェアを適用します。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := gin.Default()
// 保護されたルートにミドルウェアを適用
r.GET("/api/protected", VerifyAccessToken(), func(c *gin.Context) {
// コンテキストから直接 アクセス トークン (Access token) 情報を取得
tokenInterface, exists := c.Get("auth")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token not found"})
return
}
token := tokenInterface.(jwt.Token)
c.JSON(http.StatusOK, gin.H{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
r.Run(":8080")
}
保護された API のテスト
アクセス トークン (Access tokens) の取得
クライアントアプリケーションから: クライアント統合を設定している場合、アプリは自動的にトークンを取得できます。アクセス トークン (Access token) を抽出し、API リクエストで使用してください。
curl / Postman でのテスト用:
-
ユーザートークン: クライアントアプリの開発者ツールを使い、localStorage またはネットワークタブからアクセス トークン (Access token) をコピーします。
-
マシン間通信トークン: クライアントクレデンシャルフローを使用します。以下は curl を使った非公式な例です:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"resource
やscope
パラメーターは API リソースや権限に応じて調整が必要です。API が組織スコープの場合はorganization_id
パラメーターも必要になる場合があります。
トークンの内容を確認したい場合は、 JWT デコーダー を使って JWT をデコード・検証できます。
保護されたエンドポイントのテスト
有効なトークンリクエスト
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
期待されるレスポンス:
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
トークンなし
curl http://localhost:3000/api/protected
期待されるレスポンス (401):
{
"error": "Authorization header is missing"
}
無効なトークン
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
期待されるレスポンス (401):
{
"error": "Invalid token"
}
権限モデルごとのテスト
- グローバル API リソース
- 組織 (非 API) 権限
- 組織レベル API リソース
グローバルスコープで保護された API のテストシナリオ:
- 有効なスコープ: 必要な API スコープ(例:
api:read
,api:write
)を含むトークンでテスト - スコープ不足: 必要なスコープがない場合は 403 Forbidden を期待
- 誤ったオーディエンス: オーディエンスが API リソースと一致しない場合は 403 Forbidden を期待
# 必要なスコープがないトークン - 403 を期待
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
組織固有のアクセス制御のテストシナリオ:
- 有効な組織トークン: 正しい組織コンテキスト(組織 ID とスコープ)を含むトークンでテスト
- スコープ不足: ユーザーが要求された操作の権限を持たない場合は 403 Forbidden を期待
- 誤った組織: オーディエンスが組織コンテキスト(
urn:logto:organization:<organization_id>
)と一致しない場合は 403 Forbidden を期待
# 別の組織用トークン - 403 を期待
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
API リソース検証と組織コンテキストを組み合わせたテストシナリオ:
- 有効な組織 + API スコープ: 組織コンテキストと必要な API スコープの両方を持つトークンでテスト
- API スコープ不足: 組織トークンに必要な API 権限がない場合は 403 Forbidden を期待
- 誤った組織: 別の組織のトークンで API にアクセスした場合は 403 Forbidden を期待
- 誤ったオーディエンス: オーディエンスが組織レベルの API リソースと一致しない場合は 403 Forbidden を期待
# API スコープがない組織トークン - 403 を期待
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
さらに詳しく
実践でのロールベースのアクセス制御 (RBAC):アプリケーションのための安全な認可 (Authorization) の実装
マルチテナント SaaS アプリケーションの構築:設計から実装までの完全ガイド