跳至主要內容

如何在你的 API 服務或後端驗證存取權杖 (Access tokens)

驗證存取權杖 (Access tokens) 是在 Logto 中實施 角色型存取控制 (RBAC, Role-based access control) 的關鍵步驟。本指南將帶你逐步驗證 Logto 發行的 JWT,包括檢查簽章、簽發者 (Issuer)、受眾 (Audience)、過期時間、權限 (Scopes) 以及組織 (Organization) 上下文。

開始前須知

  • 本指南假設你已熟悉 Logto 的角色型存取控制 (RBAC) 概念。
  • 若你要保護 API 資源 (API resources),本指南假設你已完成 保護全域 API 資源 指南。
  • 若你要保護應用程式內功能或流程(非 API 權限),本指南假設你已完成 保護組織(非 API)權限 指南。
  • 若你要保護組織層級 API 資源,本指南假設你已完成 保護組織層級 API 資源 指南。

步驟 1:初始化常數與工具方法

在你的程式碼中定義必要的常數與工具函式,以處理權杖(token)的擷取與驗證。一個有效的請求必須包含 Authorization 標頭,格式為 Bearer <存取權杖 (Access token)>

auth-middleware.ts
import { IncomingHttpHeaders } from 'http';

const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
const ISSUER = 'https://your-tenant.logto.app/oidc';

export class AuthInfo {
constructor(
public sub: string,
public clientId?: string,
public organizationId?: string,
public scopes: string[] = [],
public audience: string[] = []
) {}
}

export class AuthorizationError extends Error {
name = 'AuthorizationError';
constructor(
message: string,
public status = 403
) {
super(message);
}
}

export function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string {
const bearerPrefix = 'Bearer ';

if (!authorization) {
throw new AuthorizationError('Authorization header is missing', 401);
}

if (!authorization.startsWith(bearerPrefix)) {
throw new AuthorizationError(`Authorization header must start with "${bearerPrefix}"`, 401);
}

return authorization.slice(bearerPrefix.length);
}

步驟 2:取得你的 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

步驟 3:驗證權杖與權限

在擷取權杖並取得 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) 與驗證規則有所不同:

  • 受眾宣告 (aud): API 資源標示符 (API resource indicator)
  • 組織宣告 (organization_id): 不存在
  • 權限範圍需檢查 (scope): API 資源權限 (API resource permissions)

對於非 API 組織權限,組織上下文由 aud 宣告表示 (例如 urn:logto:organization:abc123)。organization_id 宣告僅存在於組織層級 API 資源權杖中。

提示:

對於多租戶 API,務必同時驗證權限範圍 (scopes) 及上下文(受眾 (audience)、組織 (organization)),以確保安全。

新增驗證邏輯

我們在本範例中使用 jose 來驗證 JWT。如果尚未安裝,請先安裝:

npm install jose

或使用你偏好的套件管理工具(例如 pnpmyarn)。

首先,新增這些共用工具來處理 JWT 驗證:

jwt-validator.ts
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { AuthInfo, AuthorizationError } from './auth-middleware.js';

const jwks = createRemoteJWKSet(new URL(JWKS_URI));

export async function validateJwt(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});

verifyPayload(payload);
return payload;
}

export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];

return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}

function verifyPayload(payload: JWTPayload): void {
// 在此根據權限模型實作你的驗證邏輯
// 相關內容會在下方權限模型區段展示
}

接著,實作 middleware 來驗證存取權杖 (Access token):

auth-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { validateJwt, createAuthInfo } from './jwt-validator.js';

// 擴展 Express Request 介面以包含 auth
declare global {
namespace Express {
interface Request {
auth?: AuthInfo;
}
}
}

export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwt(token);

// 將驗證資訊存入 request 以便通用使用
req.auth = createAuthInfo(payload);

next();
} catch (err: any) {
return res.status(err.status ?? 401).json({ error: err.message });
}
}

根據你的權限模型,在 jwt-validator.ts 中實作相應的驗證邏輯:

jwt-validator.ts
function verifyPayload(payload: JWTPayload): void {
// 檢查 audience 宣告是否符合你的 API 資源標示符 (Resource indicator)
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Invalid audience');
}

// 檢查全域 API 資源所需的權限範圍 (Scopes)
const requiredScopes = ['api:read', 'api:write']; // 請替換為實際所需的權限範圍
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Insufficient scope');
}
}

步驟 4:將中介軟體套用到你的 API

將中介軟體套用到你受保護的 API 路由。

app.ts
import express from 'express';
import { verifyAccessToken } from './auth-middleware.js';

const app = express();

app.get('/api/protected', verifyAccessToken, (req, res) => {
// 直接從 req.auth 取得驗證 (Authentication) 資訊
res.json({ auth: req.auth });
});

app.get('/api/protected/detailed', verifyAccessToken, (req, res) => {
// 你的受保護端點邏輯
res.json({
auth: req.auth,
message: '成功存取受保護資料',
});
});

app.listen(3000);

步驟 5:測試你的實作

取得存取權杖 (Access tokens)

從你的用戶端應用程式取得: 如果你已完成用戶端整合,你的應用程式可以自動取得權杖。擷取存取權杖 (Access token) 並在 API 請求中使用。

使用 curl / Postman 測試:

  1. 使用者權杖 (User tokens): 使用你的用戶端應用程式的開發者工具,從 localStorage 或網路分頁複製存取權杖 (Access token)

  2. 機器對機器權杖 (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) 調整 resourcescope 參數;如果你的 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"
}

權限模型專屬測試

針對以全域權限範圍 (Scopes) 保護的 API 測試情境:

  • 有效權限範圍 (Valid scopes): 使用包含所需 API 權限範圍(如 api:readapi: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
自訂權杖宣告 (Customizing token claims) JSON Web Token (JWT)

OpenID Connect Discovery

RFC 8707:資源標示符 (Resource Indicators)