メインコンテンツまでスキップ

API サービスやバックエンドでアクセス トークンを検証する方法

アクセス トークンの検証は、Logto で ロールベースのアクセス制御 (RBAC) を強制するための重要な部分です。このガイドでは、バックエンド / API で Logto が発行した JWT を検証し、署名、発行者 (Issuer)、オーディエンス (Audience)、有効期限、権限 (スコープ)、組織コンテキストをチェックする方法を説明します。

始める前に

  • このガイドは、Logto の RBAC 概念に精通していることを前提としています。
  • API リソースを保護する場合、このガイドは グローバル API リソースの保護 ガイドを完了していることを前提としています。
  • アプリ内機能やワークフロー(非 API 権限)を保護する場合、このガイドは 組織(非 API)権限の保護 ガイドを完了していることを前提としています。
  • 組織レベルの API リソースを保護する場合、このガイドは 組織レベルの API リソースの保護 ガイドを完了していることを前提としています。

ステップ 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: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

ステップ 3: トークンと権限の検証

トークンを抽出し、OIDC 設定を取得した後、以下を検証します:

  • 署名:JWT が有効であり、Logto(JWKS 経由)によって署名されていること。
  • 発行者 (Issuer):Logto テナントの発行者と一致すること。
  • オーディエンス (Audience):Logto に登録された API リソースインジケーター、または該当する場合は組織コンテキストと一致すること。
  • 有効期限:トークンが有効期限切れでないこと。
  • 権限 (スコープ):API / アクションに必要なスコープがトークンに含まれていること。スコープは scope クレーム内のスペース区切り文字列です。
  • 組織コンテキスト:組織レベルの API リソースを保護する場合、organization_id クレームを検証します。

JWT の構造やクレームについて詳しくは JSON Web Token を参照してください。

各権限モデルで確認すべき内容

クレームや検証ルールは権限モデルによって異なります:

権限モデルAudience クレーム (aud)Organization クレーム (organization_id)チェックするスコープ(権限) (scope)
グローバル API リソースAPI リソースインジケーターなしAPI リソース権限
組織(非 API)権限urn:logto:organization:<id>(組織コンテキストが aud クレームに含まれる)なし組織権限
組織レベル API リソースAPI リソースインジケーター組織 ID(リクエストと一致する必要あり)API リソース権限

非 API 組織権限の場合、組織コンテキストは aud クレーム(例:urn:logto:organization:abc123)で表現されます。organization_id クレームは組織レベル API リソース トークンにのみ存在します。

ヒント:

セキュアなマルチテナント 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);

// リクエストに認証情報を格納し、汎用的に利用できるようにする
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 リソースに必要なスコープを確認
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 から直接アクセス
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 を返すこと。
トークン クレームのカスタマイズ JSON Web Token (JWT)

OpenID Connect Discovery

RFC 8707: リソースインジケーター