RBAC と JWT 検証で Actix Web API を保護する
このガイドでは、Logto が発行する ロールベースのアクセス制御 (RBAC) と JSON Web Token (JWT) を利用して、Actix Web API に認可 (Authorization) を実装し、セキュリティを強化する方法を説明します。
始める前に
クライアントアプリケーションは Logto から アクセス トークン (Access token) を取得する必要があります。まだクライアント統合を設定していない場合は、React、Vue、Angular などのクライアントフレームワーク向け クイックスタート や、サーバー間アクセス用の マシン間通信 (M2M) ガイド をご覧ください。
このガイドは、Actix Web アプリケーションにおけるこれらのトークンの サーバーサイド検証 に焦点を当てています。
学べること
- JWT 検証:アクセス トークン (Access token) を検証し、認証 (Authentication) 情報を抽出する方法
 - ミドルウェア実装:API 保護のための再利用可能なミドルウェアの作成
 - 権限モデル:さまざまな認可 (Authorization) パターンの理解と実装
- アプリケーション全体のエンドポイント向けグローバル API リソース
 - テナント固有の機能制御のための組織 (Organization) 権限
 - マルチテナントデータアクセスのための組織レベル API リソース
 
 - RBAC 統合:API エンドポイントでロールベースの権限 (Permission) とスコープ (Scope) を強制する方法
 
前提条件
- Rust の最新安定版がインストールされていること
 - Actix Web および 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 プロジェクトの初期化
新しい Actix Web プロジェクトを初期化するには、ディレクトリを作成し、基本的な構造をセットアップします:
cargo new your-api-name
cd your-api-name
Cargo.toml に Actix Web の依存関係を追加します:
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
基本的な Actix Web アプリケーションを作成します:
use actix_web::{web, App, HttpServer, Result};
use serde_json::{json, Value};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello_handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
async fn hello_handler() -> Result<web::Json<Value>> {
    Ok(web::Json(json!({ "message": "Hello from Actix Web" })))
}
開発サーバーを起動します:
cargo run
ルート、ミドルウェア、その他の機能のセットアップ方法については、 Actix Web のドキュメント を参照してください。
定数とユーティリティの初期化
トークンの抽出と検証を処理するために、コード内で必要な定数やユーティリティを定義してください。有効なリクエストには、Authorization ヘッダーが Bearer <アクセス トークン (Access token)> の形式で含まれている必要があります。
use serde::{Deserialize, Serialize};
use std::fmt;
pub const JWKS_URI: &str = "https://your-tenant.logto.app/oidc/jwks";
pub const ISSUER: &str = "https://your-tenant.logto.app/oidc";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
    pub sub: String,
    pub client_id: Option<String>,
    pub organization_id: Option<String>,
    pub scopes: Vec<String>,
    pub audience: Vec<String>,
}
impl AuthInfo {
    pub fn new(
        sub: String,
        client_id: Option<String>,
        organization_id: Option<String>,
        scopes: Vec<String>,
        audience: Vec<String>,
    ) -> Self {
        Self {
            sub,
            client_id,
            organization_id,
            scopes,
            audience,
        }
    }
}
#[derive(Debug)]
pub struct AuthorizationError {
    pub message: String,
    pub status_code: u16,
}
impl AuthorizationError {
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            status_code: 403,
        }
    }
    pub fn with_status(message: impl Into<String>, status_code: u16) -> Self {
        Self {
            message: message.into(),
            status_code,
        }
    }
}
impl fmt::Display for AuthorizationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}
impl std::error::Error for AuthorizationError {}
pub fn extract_bearer_token(authorization: Option<&str>) -> Result<&str, AuthorizationError> {
    let auth_header = authorization.ok_or_else(|| {
        AuthorizationError::with_status("Authorization ヘッダーがありません", 401)
    })?;
    if !auth_header.starts_with("Bearer ") {
        return Err(AuthorizationError::with_status(
            "Authorization ヘッダーは \"Bearer \" で始まる必要があります",
            401,
        ));
    }
    Ok(&auth_header[7..]) // 'Bearer ' プレフィックスを削除
}
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 のため、必ず権限(スコープ)とコンテキスト(オーディエンス、組織)の両方を検証してください。
検証ロジックの追加
jsonwebtoken を使用して JWT の検証を行います。必要な依存関係を Cargo.toml に追加してください:
[dependencies]
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
まず、JWT 検証を処理するための共通ユーティリティを追加します:
use crate::{AuthInfo, AuthorizationError, ISSUER, JWKS_URI};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde_json::Value;
use std::collections::HashMap;
pub struct JwtValidator {
    jwks: HashMap<String, DecodingKey>,
}
impl JwtValidator {
    pub async fn new() -> Result<Self, AuthorizationError> {
        let jwks = Self::fetch_jwks().await?;
        Ok(Self { jwks })
    }
    async fn fetch_jwks() -> Result<HashMap<String, DecodingKey>, AuthorizationError> {
        let response = reqwest::get(JWKS_URI).await.map_err(|e| {
            AuthorizationError::with_status(format!("Failed to fetch JWKS: {}", e), 401)
        })?;
        let jwks: Value = response.json().await.map_err(|e| {
            AuthorizationError::with_status(format!("Failed to parse JWKS: {}", e), 401)
        })?;
        let mut keys = HashMap::new();
        if let Some(keys_array) = jwks["keys"].as_array() {
            for key in keys_array {
                if let (Some(kid), Some(kty), Some(n), Some(e)) = (
                    key["kid"].as_str(),
                    key["kty"].as_str(),
                    key["n"].as_str(),
                    key["e"].as_str(),
                ) {
                    if kty == "RSA" {
                        if let Ok(decoding_key) = DecodingKey::from_rsa_components(n, e) {
                            keys.insert(kid.to_string(), decoding_key);
                        }
                    }
                }
            }
        }
        if keys.is_empty() {
            return Err(AuthorizationError::with_status("No valid keys found in JWKS", 401));
        }
        Ok(keys)
    }
    pub fn validate_jwt(&self, token: &str) -> Result<AuthInfo, AuthorizationError> {
        let header = decode_header(token).map_err(|e| {
            AuthorizationError::with_status(format!("Invalid token header: {}", e), 401)
        })?;
        let kid = header.kid.ok_or_else(|| {
            AuthorizationError::with_status("Token missing kid claim", 401)
        })?;
        let key = self.jwks.get(&kid).ok_or_else(|| {
            AuthorizationError::with_status("Unknown key ID", 401)
        })?;
        let mut validation = Validation::new(Algorithm::RS256);
        validation.set_issuer(&[ISSUER]);
        validation.validate_aud = false; // オーディエンスは手動で検証します
        let token_data = decode::<Value>(token, key, &validation).map_err(|e| {
            AuthorizationError::with_status(format!("Invalid token: {}", e), 401)
        })?;
        let claims = token_data.claims;
        self.verify_payload(&claims)?;
        Ok(self.create_auth_info(claims))
    }
    fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
        // 権限モデルに基づいた検証ロジックをここに実装します
        // 下記の権限モデルセクションで説明します
        Ok(())
    }
    fn create_auth_info(&self, claims: Value) -> AuthInfo {
        let scopes = claims["scope"]
            .as_str()
            .map(|s| s.split(' ').map(|s| s.to_string()).collect())
            .unwrap_or_default();
        let audience = match &claims["aud"] {
            Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect(),
            Value::String(s) => vec![s.clone()],
            _ => vec![],
        };
        AuthInfo::new(
            claims["sub"].as_str().unwrap_or_default().to_string(),
            claims["client_id"].as_str().map(|s| s.to_string()),
            claims["organization_id"].as_str().map(|s| s.to_string()),
            scopes,
            audience,
        )
    }
}
次に、アクセストークンを検証するミドルウェアを実装します:
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    web, Error, HttpMessage, HttpResponse,
};
use futures::future::{ok, Ready};
use std::sync::Arc;
pub struct JwtMiddleware {
    validator: Arc<JwtValidator>,
}
impl JwtMiddleware {
    pub fn new(validator: Arc<JwtValidator>) -> Self {
        Self { validator }
    }
}
impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = JwtMiddlewareService<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
    fn new_transform(&self, service: S) -> Self::Future {
        ok(JwtMiddlewareService {
            service,
            validator: self.validator.clone(),
        })
    }
}
pub struct JwtMiddlewareService<S> {
    service: S,
    validator: Arc<JwtValidator>,
}
impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = futures::future::LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
    forward_ready!(service);
    fn call(&self, req: ServiceRequest) -> Self::Future {
        let validator = self.validator.clone();
        Box::pin(async move {
            let authorization = req
                .headers()
                .get("authorization")
                .and_then(|h| h.to_str().ok());
            match extract_bearer_token(authorization)
                .and_then(|token| validator.validate_jwt(token))
            {
                Ok(auth_info) => {
                    // 認証情報をリクエスト拡張に保存し、汎用的に利用できるようにします
                    req.extensions_mut().insert(auth_info);
                    let fut = self.service.call(req);
                    fut.await
                }
                Err(e) => {
                    let response = HttpResponse::build(
                        actix_web::http::StatusCode::from_u16(e.status_code)
                            .unwrap_or(actix_web::http::StatusCode::FORBIDDEN),
                    )
                    .json(serde_json::json!({ "error": e.message }));
                    Ok(req.into_response(response))
                }
            }
        })
    }
}
権限モデルに応じて、JwtValidator 内で適切な検証ロジックを実装してください:
- グローバル API リソース
 - 組織(非 API)権限
 - 組織レベルの API リソース
 
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
    // オーディエンスクレームが API リソースインジケーターと一致するか確認
    let audiences = match &claims["aud"] {
        Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
        Value::String(s) => vec![s.as_str()],
        _ => vec![],
    };
    if !audiences.contains(&"https://your-api-resource-indicator") {
        return Err(AuthorizationError::new("Invalid audience"));
    }
    // グローバル API リソースに必要なスコープを確認
    let required_scopes = vec!["api:read", "api:write"]; // 実際に必要なスコープに置き換えてください
    let scopes = claims["scope"]
        .as_str()
        .map(|s| s.split(' ').collect::<Vec<_>>())
        .unwrap_or_default();
    for required_scope in &required_scopes {
        if !scopes.contains(required_scope) {
            return Err(AuthorizationError::new("Insufficient scope"));
        }
    }
    Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
    // オーディエンスクレームが組織形式と一致するか確認
    let audiences = match &claims["aud"] {
        Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
        Value::String(s) => vec![s.as_str()],
        _ => vec![],
    };
    let has_org_audience = audiences.iter().any(|aud| aud.starts_with("urn:logto:organization:"));
    if !has_org_audience {
        return Err(AuthorizationError::new("Invalid audience for organization permissions"));
    }
    // 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出が必要な場合あり)
    let expected_org_id = "your-organization-id"; // リクエストコンテキストから抽出
    let expected_aud = format!("urn:logto:organization:{}", expected_org_id);
    if !audiences.contains(&expected_aud.as_str()) {
        return Err(AuthorizationError::new("Organization ID mismatch"));
    }
    // 必要な組織スコープを確認
    let required_scopes = vec!["invite:users", "manage:settings"]; // 実際に必要なスコープに置き換えてください
    let scopes = claims["scope"]
        .as_str()
        .map(|s| s.split(' ').collect::<Vec<_>>())
        .unwrap_or_default();
    for required_scope in &required_scopes {
        if !scopes.contains(required_scope) {
            return Err(AuthorizationError::new("Insufficient organization scope"));
        }
    }
    Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
    // オーディエンスクレームが API リソースインジケーターと一致するか確認
    let audiences = match &claims["aud"] {
        Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
        Value::String(s) => vec![s.as_str()],
        _ => vec![],
    };
    if !audiences.contains(&"https://your-api-resource-indicator") {
        return Err(AuthorizationError::new("Invalid audience for organization-level API resources"));
    }
    // 組織 ID がコンテキストと一致するか確認(リクエストコンテキストから抽出が必要な場合あり)
    let expected_org_id = "your-organization-id"; // リクエストコンテキストから抽出
    let org_id = claims["organization_id"].as_str().unwrap_or_default();
    if expected_org_id != org_id {
        return Err(AuthorizationError::new("Organization ID mismatch"));
    }
    // 組織レベルの API リソースに必要なスコープを確認
    let required_scopes = vec!["api:read", "api:write"]; // 実際に必要なスコープに置き換えてください
    let scopes = claims["scope"]
        .as_str()
        .map(|s| s.split(' ').collect::<Vec<_>>())
        .unwrap_or_default();
    for required_scope in &required_scopes {
        if !scopes.contains(required_scope) {
            return Err(AuthorizationError::new("Insufficient organization-level API scopes"));
        }
    }
    Ok(())
}
ミドルウェアを API に適用する
これで、保護された API ルートにミドルウェアを適用します。
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(validator.clone()))
            .wrap(Logger::default())
            .service(
                web::scope("/api/protected")
                    .wrap(JwtMiddleware::new(validator.clone()))
                    .route("", web::get().to(protected_handler))
            )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
    // リクエスト拡張から認証情報 (AuthInfo) を取得
    let auth = req.extensions().get::<AuthInfo>().unwrap();
    Ok(web::Json(json!({ "auth": auth })))
}
保護された 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 アプリケーションの構築:設計から実装までの完全ガイド