跳至主要內容

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

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

開始之前

步驟 1:初始化常數與工具函式

在你的程式碼中定義必要的常數與工具函式,用於處理權杖提取與驗證。有效的請求必須包含 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) 或組織上下文相符(如適用)。
  • 過期時間 (Expiration): 權杖不得過期。
  • 權限 (Scopes): 權杖必須包含你 API / 行為所需的權限範圍。Scopes 會以空格分隔字串出現在 scope 宣告 (Claim)。
  • 組織上下文 (Organization context): 若保護的是組織層級 API 資源,需驗證 organization_id 宣告 (Claim)。

想了解更多 JWT 結構與宣告,請參閱 JSON Web Token

各權限模型需檢查的項目

不同權限模型下,宣告與驗證規則如下:

權限模型受眾宣告 (aud)組織宣告 (organization_id)權限(需檢查的 scopes) (scope)
全域 API 資源API 資源標示符 (Resource indicator)API 資源權限
組織(非 API)權限urn:logto:organization:<id>(組織上下文於 aud 宣告)組織權限
組織層級 API 資源API 資源標示符 (Resource indicator)組織 ID(必須與請求相符)API 資源權限

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

提示:

務必同時驗證權限(scopes)與上下文(受眾、組織)以確保多租戶 API 的安全性。

新增驗證邏輯

本範例中我們使用 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 validateJwtToken(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 {
// 根據權限模型實作你的驗證邏輯
// 相關內容會在下方權限模型章節展示
}

接著,實作中介軟體來驗證存取權杖 (Access token):

auth-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, 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 validateJwtToken(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 資源標示符
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:將中介軟體 (Middleware) 套用到你的 API

將中介軟體 (Middleware) 套用到你要保護的 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:測試你的實作

使用有效與無效權杖測試你的 API,確保:

  • 有效權杖可通過並取得存取權。
  • 無效或缺少權杖時回傳 401 Unauthorized。若權杖有效但缺少必要權限或上下文,回傳 403 Forbidden
自訂權杖宣告 (Claim) JSON Web Token (JWT)

OpenID Connect 探索 (Discovery)

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