使用 RBAC 和 JWT 驗證 (JWT validation) 保護你的 Rocket API
本指南將協助你透過 角色型存取控制 (RBAC, Role-based Access Control) 以及 Logto 簽發的 JSON Web Token (JWT) 實作授權 (Authorization),以保護你的 Rocket API。
開始前
你的用戶端應用程式需要從 Logto 取得存取權杖 (Access tokens)。如果你尚未完成用戶端整合,請參考我們針對 React、Vue、Angular 或其他前端框架的 快速入門,或伺服器對伺服器存取請參閱 機器對機器指南。
本指南聚焦於在你的 Rocket 應用程式中,對這些權杖進行伺服器端驗證。

你將學到
- JWT 驗證: 學習如何驗證存取權杖 (Access tokens) 並擷取驗證 (Authentication) 資訊
- 中介軟體實作: 建立可重複使用的中介軟體以保護 API
- 權限模型: 理解並實作不同的授權 (Authorization) 模式:
- 全域 API 資源 (Global API resources) 用於應用程式層級端點
- 組織權限 (Organization permissions) 控制租戶專屬功能
- 組織層級 API 資源 (Organization-level API resources) 用於多租戶資料存取
- RBAC 整合: 在 API 端點強制執行角色型權限 (Role-based permissions) 與權限範圍 (Scopes)
先決條件
- 已安裝最新版穩定版 Rust
- 基本了解 Rocket 與 Web API 開發
- 已設定 Logto 應用程式(如有需要請參閱 快速入門)
權限 (Permission) 模型總覽
在實作保護機制前,請先選擇最適合你應用程式架構的權限模型。這與 Logto 的三大授權 (Authorization) 情境相符:
- 全域 API 資源 (Global API resources)
- 組織(非 API)權限 (Organization (non-API) permissions)
- 組織層級 API 資源 (Organization-level API resources)

- 適用情境: 保護整個應用程式共用的 API 資源(非組織專屬)
- 權杖類型: 具有全域受眾 (global audience) 的存取權杖 (Access token)
- 範例: 公開 API、核心產品服務、管理端點
- 最適用於: 所有客戶共用 API 的 SaaS 產品、無租戶隔離的微服務架構
- 深入瞭解: 保護全域 API 資源

- 適用情境: 控制組織專屬的操作、UI 功能或商業邏輯(非 API)
- 權杖類型: 具有組織專屬受眾 (organization-specific audience) 的組織權杖 (Organization token)
- 範例: 功能開關、儀表板權限、成員邀請控制
- 最適用於: 具有組織專屬功能與流程的多租戶 SaaS
- 深入瞭解: 保護組織(非 API)權限

- 適用情境: 保護僅在特定組織情境下可存取的 API 資源
- 權杖類型: 具有 API 資源受眾 (API resource audience) + 組織情境 (organization context) 的組織權杖 (Organization token)
- 範例: 多租戶 API、組織範圍資料端點、租戶專屬微服務
- 最適用於: API 資料以組織為範圍的多租戶 SaaS
- 深入瞭解: 保護組織層級 API 資源
💡 請在繼續前選擇你的模型 —— 本指南後續內容將以你選擇的方式為參考。
快速準備步驟
設定 Logto 資源與權限 (Permissions)
- 全域 API 資源 (Global API resources)
- 組織(非 API)權限 (Organization (non-API) permissions)
- 組織層級 API 資源 (Organization-level API resources)
- 建立 API 資源 (API resource): 前往 Console → API 資源 (API resources) 並註冊你的 API(例如:
https://api.yourapp.com
) - 定義權限 (Permissions): 新增如
read:products
、write:orders
等權限範圍 (Scopes) —— 參考 定義帶有權限的 API 資源 - 建立全域角色 (Global roles): 前往 Console → 角色 (Roles) 並建立包含 API 權限的角色 —— 參考 設定全域角色
- 指派角色 (Assign roles): 將角色指派給需要 API 存取權的使用者或 M2M 應用程式
- 定義組織權限 (Organization permissions): 在組織範本中建立如
invite:member
、manage:billing
等非 API 組織權限 - 設定組織角色 (Organization roles): 在組織範本中配置組織專屬角色並指派權限給這些角色
- 指派組織角色 (Assign organization roles): 在每個組織情境下將使用者指派到組織角色
- 建立 API 資源 (API resource): 如上註冊你的 API 資源,但將用於組織情境
- 定義權限 (Permissions): 新增如
read:data
、write:settings
等限定於組織情境的權限範圍 (Scopes) - 設定組織範本 (Configure organization template): 設定包含 API 資源權限的組織角色
- 指派組織角色 (Assign organization roles): 將使用者或 M2M 應用程式指派到包含 API 權限的組織角色
- 多租戶設定 (Multi-tenant setup): 確保你的 API 能處理組織範圍的資料與驗證
建議從我們的 角色型存取控制 (RBAC) 指南 開始,獲得逐步設定說明。
更新你的用戶端應用程式
在用戶端請求適當的權限範圍 (Scopes):
- 使用者驗證 (Authentication):更新你的應用程式 → 以請求你的 API 權限範圍和/或組織情境
- 機器對機器 (M2M):設定 M2M 權限範圍 → 以進行伺服器對伺服器存取
通常需要在用戶端設定中新增以下一項或多項:
- OAuth 流程中的
scope
參數 - 用於 API 資源存取的
resource
參數 - 組織情境下的
organization_id
請確保你測試的使用者或 M2M 應用程式已被指派包含所需 API 權限的正確角色或組織角色。
初始化你的 API 專案
要初始化一個新的 Rocket 專案,請建立一個目錄並設置基本結構:
cargo new your-api-name
cd your-api-name
將 Rocket 相關依賴加入你的 Cargo.toml
:
[dependencies]
rocket = { version = "0.5", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
建立一個基本的 Rocket 應用程式:
use rocket::{get, launch, routes, serde::json::Json};
use serde_json::{json, Value};
#[get("/")]
fn hello_handler() -> Json<Value> {
Json(json!({ "message": "Hello from Rocket" }))
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![hello_handler])
}
啟動開發伺服器:
cargo run
如需更多關於如何設置路由、請求守衛及其他功能的詳細資訊,請參考 Rocket 官方文件。
初始化常數與工具函式
在你的程式碼中定義必要的常數與工具函式,以處理權杖(token)的擷取與驗證。一個有效的請求必須包含 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 header is missing", 401)
})?;
if !auth_header.starts_with("Bearer ") {
return Err(授權錯誤 (AuthorizationError)::with_status(
"Authorization header must start with \"Bearer \"",
401,
));
}
Ok(&auth_header[7..]) // 移除 'Bearer ' 前綴
}
取得你的 Logto 租戶資訊
你需要以下數值來驗證 Logto 發行的權杖:
- JSON Web Key Set (JWKS) URI:Logto 公鑰的網址,用於驗證 JWT 簽章。
- 簽發者 (Issuer):預期的簽發者值(Logto 的 OIDC URL)。
首先,找到你的 Logto 租戶端點。你可以在多個地方找到:
- 在 Logto Console,設定 → 網域。
- 在你於 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 設定後,請驗證以下項目:
- 簽章 (Signature): JWT 必須有效且由 Logto(透過 JWKS)簽署。
- 簽發者 (Issuer): 必須符合你的 Logto 租戶簽發者。
- 受眾 (Audience): 必須符合在 Logto 註冊的 API 資源標示符 (resource indicator),或在適用時符合組織 (Organization) 上下文。
- 過期時間 (Expiration): 權杖不得過期。
- 權限範圍 (Permissions, scopes): 權杖必須包含 API/操作所需的權限範圍 (scopes)。scopes 會以空格分隔字串出現在
scope
宣告 (claim) 中。 - 組織 (Organization) 上下文: 若保護的是組織層級 API 資源,需驗證
organization_id
宣告 (claim)。
詳情請參閱 JSON Web Token 以瞭解 JWT 結構與宣告 (claims)。
各權限模型需檢查的項目
不同權限模型下,宣告 (claims) 與驗證規則有所不同:
- 全域 API 資源
- 組織(非 API)權限
- 組織層級 API 資源
- 受眾宣告 (
aud
): API 資源標示符 (API resource indicator) - 組織宣告 (
organization_id
): 不存在 - 權限範圍需檢查 (
scope
): API 資源權限 (API resource permissions)
- 受眾宣告 (
aud
):urn:logto:organization:<id>
(組織上下文於aud
宣告中) - 組織宣告 (
organization_id
): 不存在 - 權限範圍需檢查 (
scope
): 組織權限 (Organization permissions)
- 受眾宣告 (
aud
): API 資源標示符 (API resource indicator) - 組織宣告 (
organization_id
): 組織 ID(必須與請求相符) - 權限範圍需檢查 (
scope
): API 資源權限 (API resource permissions)
對於非 API 組織權限,組織上下文由 aud
宣告表示 (例如
urn:logto:organization:abc123
)。organization_id
宣告僅存在於組織層級 API 資源權杖中。
對於多租戶 API,務必同時驗證權限範圍 (scopes) 及上下文(受眾 (audience)、組織 (organization)),以確保安全。
新增驗證邏輯
我們使用 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; // 我們將手動驗證 audience
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,
)
}
}
接著,實作中介軟體來驗證存取權杖 (Access token):
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use rocket::{
http::Status,
outcome::Outcome,
request::{self, FromRequest, Request},
State,
};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthInfo {
type Error = AuthorizationError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let validator = match req.guard::<&State<JwtValidator>>().await {
Outcome::Success(validator) => validator,
Outcome::Failure((status, _)) => {
return Outcome::Failure((
status,
AuthorizationError::with_status("找不到 JWT validator (JWT validator not found)", 500),
))
}
Outcome::Forward(()) => {
return Outcome::Forward(())
}
};
let authorization = req.headers().get_one("authorization");
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt(token))
{
Ok(auth_info) => Outcome::Success(auth_info),
Err(e) => {
let status = Status::from_code(e.status_code).unwrap_or(Status::Forbidden);
Outcome::Failure((status, e))
}
}
}
}
根據你的權限模型,在 JwtValidator
中實作對應的驗證邏輯:
- 全域 API 資源 (Global API resources)
- 組織(非 API)權限 (Organization (non-API) permissions)
- 組織層級 API 資源 (Organization-level API resources)
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// 檢查 audience 宣告是否符合你的 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 資源所需的權限範圍 (Scopes)
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> {
// 檢查 audience 宣告是否符合組織格式
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> {
// 檢查 audience 宣告是否符合你的 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 rocket::{get, launch, routes, serde::json::Json};
use serde_json::{json, Value};
mod lib;
mod jwt_validator;
mod guards;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[get("/api/protected")]
fn protected_handler(auth: AuthInfo) -> Json<Value> {
// 直接從請求守衛 (request guard) 取得驗證 (Authentication) 資訊
Json(json!({ "auth": auth }))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("初始化 JWT 驗證器失敗");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler])
}
測試你的受保護 API
取得存取權杖 (Access tokens)
從你的用戶端應用程式取得: 如果你已完成用戶端整合,你的應用程式可以自動取得權杖。擷取存取權杖 (Access token) 並在 API 請求中使用。
使用 curl / Postman 測試:
-
使用者權杖 (User tokens): 使用你的用戶端應用程式的開發者工具,從 localStorage 或網路分頁複製存取權杖 (Access token)
-
機器對機器權杖 (Machine-to-machine tokens): 使用 client credentials flow。以下是使用 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"你可能需要根據你的 API 資源 (API resource) 和權限 (Permissions) 調整
resource
和scope
參數;如果你的 API 以組織 (Organization) 為範圍,也可能需要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 資源 (Global API resources)
- 組織(非 API)權限 (Organization (non-API) permissions)
- 組織層級 API 資源 (Organization-level API resources)
針對以全域權限範圍 (Scopes) 保護的 API 測試情境:
- 有效權限範圍 (Valid scopes): 使用包含所需 API 權限範圍(如
api:read
、api:write
)的權杖測試 - 缺少權限範圍 (Missing scopes): 權杖缺少必要權限範圍時,預期回傳 403 Forbidden
- 錯誤受眾 (Wrong audience): 權杖受眾 (Audience) 不符合 API 資源時,預期回傳 403 Forbidden
# 權杖缺少必要權限範圍 - 預期 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
針對組織專屬存取控制的測試情境:
- 有效組織權杖 (Valid organization token): 使用包含正確組織 (Organization) 資訊(組織 ID 與權限範圍)的權杖測試
- 缺少權限範圍 (Missing scopes): 使用者沒有執行請求操作的權限時,預期回傳 403 Forbidden
- 錯誤組織 (Wrong organization): 權杖受眾 (Audience) 不符合組織上下文(
urn:logto:organization:<organization_id>
)時,預期回傳 403 Forbidden
# 錯誤組織的權杖 - 預期 403
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
結合 API 資源驗證與組織上下文的測試情境:
- 有效組織 + API 權限範圍 (Valid organization + API scopes): 權杖同時具備組織上下文與所需 API 權限範圍時測試
- 缺少 API 權限範圍 (Missing API scopes): 組織權杖缺少必要 API 權限時,預期回傳 403 Forbidden
- 錯誤組織 (Wrong organization): 使用不同組織的權杖存取 API 時,預期回傳 403 Forbidden
- 錯誤受眾 (Wrong audience): 權杖受眾 (Audience) 不符合組織層級 API 資源時,預期回傳 403 Forbidden
# 組織權杖缺少 API 權限範圍 - 預期 403
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
延伸閱讀
RBAC 實務應用:為你的應用程式實現安全授權 (Authorization)
建立多租戶 SaaS 應用程式:從設計到實作的完整指南