API設計と認証・認可

目次

概要

接続点は壊れにくく、誤用されにくく設計する

APIは単にデータを返す口ではなく、システム境界そのものです。使いやすさ、互換性、認証、認可、エラー表現、観測性まで含めて設計します。

要点

良いAPIは「呼べる」だけでなく、「誤用しにくい」「壊しにくい」「権限の境界が明確」という性質を持ちます。

この章で重視すること

API設計の基本

まず決めるべきなのは、何を資源として扱い、どこまでを操作単位にするかです。RESTGraphQLgRPCは表現や通信の形が違うだけで、境界の設計そのものが不要になるわけではありません。

押さえるべき論点は次です。

  • URLやメソッドの意味
  • IDの安定性
  • ページング
  • フィルタと並び替え
  • 冪等性
  • 部分更新
  • エラー形式

HTTP APIでは、独自の雰囲気だけで設計せず、HTTPメソッド、ステータスコード、キャッシュ、条件付きリクエスト、認証ヘッダーの意味を標準に寄せます。とくに公開APIでは、実装の都合よりも「利用者が予測できること」を優先します。

OpenAPIのような機械可読な仕様を用意すると、ドキュメント、SDK生成、モック、契約テスト、レビューを同じ定義から始められます。API仕様は納品物ではなく、変更時に一緒に更新される設計資産として扱います。

REST

RESTでは、resourceをURLとして表し、HTTP methodで操作を表します。重要なのは、URLの見た目よりもmethodの意味、status code、cache、冪等性を一貫して扱うことです。

例:

GET    /users/123
PATCH  /users/123
DELETE /users/123
POST   /orders

GET は副作用を持たせない、PUT は置き換え、PATCH は部分更新、POST は新規作成や処理開始、というように意味をそろえます。

GraphQL

GraphQLは、クライアントが必要な形を問い合わせやすい点が強みです。複数画面やフロントエンドの要求が多様な場合に便利です。一方で、権限、N+1、query cost、schema evolutionをきちんと設計しないと運用が難しくなります。

gRPC

gRPCはschema-firstで、Protocol Buffersを使って型付きのRPCを定義します。内部サービス間通信や高頻度通信に向きます。HTTP/JSONより人間が直接読みやすいわけではないため、公開APIと内部APIで使い分けることがあります。

HTTPの意味を設計に使う

HTTP APIを設計するときは、HTTPを単なる通信路ではなく、意味を持ったアプリケーションプロトコルとして使います。RFC 9110は、method、status code、header、cache、content negotiationなどの意味を定義しています。独自ルールで上書きしすぎると、クライアント、proxy、cache、監視、SDKがHTTPの前提を使えなくなります。

methodの性質

method 主な用途 安全性 冪等性 設計上の注意
GET 取得 safe idempotent 副作用を持たせない
POST 作成、処理開始 unsafe 原則非冪等 idempotency keyを検討する
PUT resource全体の置き換え unsafe idempotent 部分更新と混同しない
PATCH 部分更新 unsafe 実装次第 patch形式と競合制御を決める
DELETE 削除 unsafe idempotentに設計しやすい 物理削除か論理削除かを決める

ここでいうsafeは「読み取りであり、利用者が要求した状態変化を起こさない」という意味です。ログ記録やメトリクス更新のような内部的な副作用はありますが、注文作成や残高変更のような業務上の副作用をGETに入れてはいけません。

status codeを情報設計として使う

RFC 9110では、クライアントは未知のstatus codeでも先頭桁のclassを理解する必要があります。つまり、2xx3xx4xx5xxの分類はAPI利用者にとって重要なシグナルです。

2xx:
  要求は受け入れられた、または成功した

4xx:
  呼び出し側が修正すべき問題

5xx:
  サーバ側、依存先、または一時的な障害

アプリケーション固有の詳細はbodyのcodeで表し、HTTP status codeは大分類として使うと、監視やretry policyが組みやすくなります。

エラー設計

APIのエラーは、開発者体験と運用性に直結します。

含めたい情報は次です。

  • machine-readableなerror code
  • 人間が読めるmessage
  • request id / trace id
  • retry可能か
  • validation errorのフィールド

ただし、内部実装や機密情報を返してはいけません。

認証失敗、認可失敗、入力エラー、競合、rate limit、内部エラーは、利用者が次に取るべき行動が違います。すべてを400500に寄せると、クライアントは再試行すべきか、入力を直すべきか、権限を確認すべきか判断できません。

状況 代表的な扱い 利用者が取る行動
入力が不正 400 / validation error 入力を修正する
認証されていない 401 ログインまたはtoken更新
権限がない 403 権限付与やresource確認
対象がない 404 IDや公開範囲を確認
競合している 409 最新状態を取得して再実行
rate limit 429 待って再試行
サーバ内部 5xx retry policyに従う

契約としてのAPI

APIは実装の入口ではなく、利用者との契約です。契約には、URL、method、schema、認証方式、エラー形式、rate limit、廃止方針が含まれます。

flowchart LR Spec["OpenAPI / Schema"] Mock["Mock / SDK"] Impl["Implementation"] ContractTest["Contract Test"] Docs["Documentation"] Spec --> Mock Spec --> Impl Spec --> ContractTest Spec --> Docs

契約を中心に置くと、フロントエンド、バックエンド、外部利用者、テストが同じ前提を共有できます。仕様と実装がずれると、APIは静かに壊れます。

互換性の基本

公開済みAPIで安全に行いやすい変更:

  • optional fieldを追加する
  • enumを追加する。ただしunknown値に耐えられる設計にする
  • 新しいendpointを追加する
  • エラーに補助情報を追加する

危険な変更:

  • 既存fieldを削除する
  • 型を変える
  • 必須fieldを増やす
  • status codeの意味を変える
  • 認可条件を暗黙に変える

API設計では、最初の実装よりも、2回目以降の変更で壊れないことが重要です。

OpenAPIで何を記述するか

OpenAPI Specificationは、HTTP APIを人間と機械が理解できる形で記述する仕様です。仕様には、paths、operations、parameters、request body、responses、schemas、security schemesなどを含めます。

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders/{orderId}:
    get:
      operationId: getOrder
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Order found
        "404":
          description: Order not found

OpenAPIのresponsesは、成功だけでなく、分かっているエラーも書きます。仕様では、少なくとも1つのresponse codeが必要で、既知のエラーを個別に定義し、未知のエラーにはdefaultを使えます。これにより、クライアントは失敗時の分岐を事前に実装できます。

セキュリティ方式

OpenAPIでは、API key、HTTP authentication、mutual TLS、OAuth2、OpenID Connectなどをsecurity schemeとして表せます。ここで重要なのは「認証方式をドキュメントに書く」だけではなく、operationごとにどのschemeとscopeが必要かを明示することです。

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

paths:
  /me:
    get:
      security:
        - bearerAuth: []

仕様に認証方式が書かれていないAPIは、SDK生成やテストで安全側に倒しにくくなります。

バージョニング

APIは一度使われると、簡単には変えられません。破壊的変更を避けるため、互換性の方針を決めます。

  • optional fieldを追加する
  • unknown fieldを無視できるようにする
  • enumの追加に備える
  • 古いversionの終了時期を明示する
  • consumer-driven contractを使う

バージョニングは「URLにv1を入れるか」だけではありません。破壊的変更をどう定義し、どの期間サポートし、どのように移行を知らせるかまで含めます。

Deprecation policy:
  - 新APIを先に公開する
  - 旧APIにdeprecation headerや告知を出す
  - 利用状況を観測する
  - 移行期限を明示する
  - 期限後に段階的に停止する

認証と認可

認証は「誰か」を確認すること、認可は「何ができるか」を決めることです。ここを混同すると、権限設計が壊れます。

実務では、次の分離が重要です。

  • 認証 password、passkey、federation、token発行
  • セッション / token管理 有効期限、失効、更新
  • 認可 role、attribute、resource単位の判断

OAuthとOpenID Connect

OAuthはdelegated authorizationのための枠組みで、OpenID Connectはその上でidentity情報を扱う層です。ログインを作るときにOAuthとOIDCを混同すると、access tokenとID tokenの扱いを誤りやすくなります。

  • access token APIへのアクセス権を表す
  • ID token 認証結果とユーザー情報を表す
  • refresh token 新しいaccess tokenを得るために使う

Webアプリケーションではauthorization code flow + PKCEを基本に考えます。

認可モデル

  • RBAC roleに権限を集める
  • ABAC 属性にもとづいて判断する
  • ReBAC 関係性にもとづいて判断する

単純なシステムではRBACで十分なことが多いですが、組織、所有関係、共有、委任が複雑になると、resource単位の認可が必要になります。

BOLAを防ぐ

OWASP API Security Top 10のAPI1:2023はBroken Object Level Authorizationです。これは、API利用者がURL、query、bodyなどに含まれるobject IDを操作し、他人のresourceへアクセスできてしまう問題です。

GET /shops/{shopId}/revenue

危険:
  ログイン済みならshopIdをそのまま信じる

必要:
  現在の利用者が、そのshopIdに対してread権限を持つかを確認する

BOLA対策では、IDを推測しにくくするだけでは足りません。UUIDを使っても、漏れたIDや共有URL経由で権限チェックを迂回できる場合があります。すべてのresourceアクセスで、主体、操作、対象resourceの組み合わせを確認します。

flowchart LR User["Subject: user/service"] Action["Action: read/update/delete"] Resource["Resource: order/shop/document"] Policy["Authorization Policy"] Decision{"Allow?"} User --> Policy Action --> Policy Resource --> Policy Policy --> Decision

オブジェクト属性レベルの認可

OWASP API3:2023は、objectのfield単位の認可不備です。たとえば管理者だけが見られるcostPriceinternalNoteを、一般ユーザー向けAPIが返してしまうケースです。

{
  "id": "order_123",
  "total": 5000,
  "internalRiskScore": 92
}

API設計では、DB modelをそのままresponseにしないことが重要です。利用者、権限、画面用途に応じてDTOやview modelを分けます。

レート制限と不正利用対策

公開APIでは、正しい認証だけでは足りません。大量リクエスト、bot、credential stuffing、スクレイピング、誤実装から守る必要があります。

見るべき観点は次です。

  • client / user / tokenごとのrate limit
  • burstとsustained trafficの区別
  • idempotency key
  • replay protection
  • request signing
  • audit log

OWASP API2:2023はBroken Authenticationです。認証endpoint、password reset、token refresh、メールアドレス変更のようなsensitive operationは攻撃対象になりやすいので、通常APIより厳しく扱います。

通常API:
  userごとのrate limit
  tokenごとのrate limit

認証API:
  IP / account / device / credential pairごとの制限
  credential stuffing検知
  MFAや再認証
  password reset abuse対策

GraphQLやbatch APIでは、1リクエスト内に複数操作を詰められるため、HTTPリクエスト数だけでrate limitするとすり抜けが起きます。operation数、query complexity、対象resource数も制限します。

設計上の注意

  • tokenに権限を持たせすぎない
  • API gatewayだけで完結したつもりにならない
  • バックエンドでもresource単位で認可確認する
  • エラーから過剰な情報を漏らさない

APIインベントリ

OWASP API9:2023はImproper Inventory Managementです。どのAPIが存在し、誰が使い、どのversionが生きていて、どのdataを扱うかが分からない状態は、それ自体がリスクです。

API inventoryには次を含めます。

  • endpointとowner
  • versionとdeprecation status
  • 認証方式
  • 必要なscope/role
  • 扱うdata分類
  • external/internalの区分
  • last accessと主要consumer
  • OpenAPI specへのリンク

古いAPI、実験用API、管理用APIは、ドキュメントから漏れた瞬間に守られにくくなります。

APIの観測性

APIは外部との契約なので、失敗したときに追える必要があります。

  • request id
  • structured log
  • latency histogram
  • status codeごとの集計
  • consumerごとのerror rate
  • schema validation error

認証・認可の失敗も、ユーザー体験とセキュリティ調査の両方から観測できるようにします。

REST APIセキュリティの設計レビュー

OWASP REST Security Cheat Sheetでは、REST APIの安全性をHTTPS、access control、JWT、API key、HTTP method制限、content type、management endpoint、error handling、audit log、security headers、CORSなど複数の観点で整理しています。API設計では、これらを実装後のチェック項目ではなく、設計レビューの入力にします。

flowchart TD API["API endpoint"] TLS["HTTPS / TLS"] Authn["Authentication"] Authz["Authorization"] Input["Input & content type"] Error["Error handling"] Log["Audit log"] Inventory["Inventory"] API --> TLS API --> Authn API --> Authz API --> Input API --> Error API --> Log API --> Inventory

レビュー観点

観点 確認すること
HTTPS credential、token、API keyが平文で流れない
access control endpointごとに認可を行い、object IDを信用しない
JWT 署名、issuer、audience、有効期限、失効方針を確認する
API key 利用者識別やquotaには使えても、単独の強い認証として過信しない
HTTP method 不要なmethodを許可しない
content type request/responseのcontent typeを検証する
error handling 内部実装やstack traceを返さない
audit log 認証、認可、管理操作、重要resource操作を追える
CORS 許可origin、credential送信、preflightを明示する

特に管理用endpointは、通常のユーザー向けAPIより強い認可、ネットワーク制限、監査ログが必要です。/admin/internal/debug/metrics のようなendpointが公開経路に混ざると事故につながります。

REST API 設計の実践的ガイド

OpenAPI 仕様(spec.openapis.org)は、REST API の標準化書式です。

OpenAPI 3.0 の基本構造

openapi: 3.0.0
info:
  title: API Title
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
paths:
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
        '404':
          description: User not found

OpenAPI を定義することで、クライアントコード生成、ドキュメント自動生成、モック API 自動生成が実現できます。

GraphQL vs REST

属性 REST GraphQL
クエリ方式 固定エンドポイント 柔軟なクエリ言語
オーバーフェッチ あり(不要なフィールド含む) なし(必要なフィールドのみ)
アンダーフェッチ あり(複数リクエスト必要) なし(1リクエストで多くの関連データ)
キャッシング HTTP キャッシュ活用可能 クエリごと異なるため複雑
デバッグ 比較的容易 クエリの複雑性が増す可能性
ラーニングカーブ 高(GraphQL スキーマ習得必要)

spec.graphql.orgGraphQL 仕様が定められており、query, mutation, subscription が基本操作です。

認証方式の比較と実装

OAuth 2.0 フロー

OpenID.net では、OAuth 2.0 の複数フロー(Authorization Code, Implicit, Resource Owner, Client Credentials)が定義されています。

Authorization Code フロー(最も安全):

1. ユーザーがログインボタンをクリック
   ↓
2. Authorization Server にリダイレクト
   ↓
3. ユーザーが許可
   ↓
4. Authorization Code を返却
   ↓
5. Backend が Code と Secret で Token Exchange
   ↓
6. Access Token 取得
   ↓
7. Access Token で Resource Access

JWT (JSON Web Token) の実装

RFC 7519 (JWT) では、Header.Payload.Signature の形式が定義されています。

import jwt
import json
from datetime import datetime, timedelta

# JWT の作成
payload = {
    'user_id': 123,
    'username': 'alice',
    'exp': datetime.utcnow() + timedelta(hours=1),
    'iat': datetime.utcnow()
}

secret = 'your-secret-key'
token = jwt.encode(payload, secret, algorithm='HS256')

# JWT の検証
try:
    decoded = jwt.decode(token, secret, algorithms=['HS256'])
    print(decoded)
except jwt.ExpiredSignatureError:
    print("Token expired")
except jwt.InvalidTokenError:
    print("Invalid token")

NIST 認証ガイドライン

csrc.nist.gov の NIST SP 800-63B では、認証強度の分類が定義されています。

レベル 要件
AAL1(Assurance Level 1) 単一要素認証 パスワード のみ
AAL2 多要素認証 パスワード + OTP
AAL3 多要素認証(推奨) パスワード + 生体認証 or ハードウェアトークン

モダン実装では、少なくとも AAL2(多要素認証)が推奨されます。

API セキュリティの実装

OWASP API Security Top 10 では、API 固有のセキュリティリスクが列挙されています。

主要なセキュリティ対策

# 1. Rate Limiting(DDoS 対策)
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)

@app.route('/api/users')
@limiter.limit("100/hour")
def get_users():
    return {...}

# 2. CORS の適切な設定
from flask_cors import CORS
CORS(app, resources={
    r"/api/*": {
        "origins": ["https://trusted-domain.com"],
        "methods": ["GET", "POST"]
    }
})

# 3. Input Validation
from pydantic import BaseModel, validator

class UserInput(BaseModel):
    name: str
    email: str
    
    @validator('email')
    def email_must_be_valid(cls, v):
        if '@' not in v:
            raise ValueError('Invalid email')
        return v

# 4. SQL Injection 対策(ORM 使用)
user = User.query.filter_by(email=user_input.email).first()
# Raw SQL は避ける

# 5. Authentication Token の保護
headers = {
    'Authorization': f'Bearer {access_token}'
}
# HTTPS 強制(HTTP では送信しない)

API バージョニング戦略

API の後方互換性を保つため、バージョニングが必要です。

バージョニング方式

1. URL パス
   /api/v1/users
   /api/v2/users

2. クエリパラメータ
   /api/users?version=2

3. ヘッダ
   Accept: application/vnd.myapi.v2+json

4. コンテンツネゴシエーション
   Accept: application/json; version=2

ほとんどのプロジェクトでは、URL パスベースのバージョニングが採用されています。

サンセット戦略

v1: 廃止予定(1年以内)
v2: アクティブ
v3: 最新

段階的な廃止:
1. 廃止通知(6ヶ月前)
2. 非推奨ヘッダ追加(Deprecation: true3. 段階的なサンセット
4. 最終的に削除

gRPC の活用

Google が開発した gRPC は、Protocol Buffers を使った高性能 RPC フレームワークです。

REST vs gRPC

属性 REST gRPC
プロトコル HTTP/1.1 HTTP/2
形式 JSON Protocol Buffers(バイナリ
性能 高(バイナリで高速)
ブラウザ対応 良好 要 gRPC-Web
デバッグ 容易(JSON テキスト) 複雑(バイナリ)
学習コスト 中(IDL 定義習得)

grpc.io では、Protocol Buffers の仕様と複数言語の実装が提供されています。

gRPC の基本実装

syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (Empty) returns (stream User);
}

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

message Empty {}

W3C 標準との統合

W3.orgCORS(Cross-Origin Resource Sharing)仕様では、クロスオリジンリクエストの安全な処理が定義されています。

CORS ヘッダ

リクエスト(ブラウザから):
Origin: https://example.com

レスポンス(サーバーから):
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600

CORS 設定を厳しくしすぎると API の利用者が増えず、緩すぎるとセキュリティリスクが高まります。

キャッシング戦略

API レスポンスのキャッシングは、パフォーマンス向上に重要です。

HTTP キャッシュヘッダ

Cache-Control: public, max-age=36003600秒(1時間)、パブリックキャッシュ有効

Cache-Control: private, max-age=300300秒(5分)、プライベート(ブラウザのみ)

Cache-Control: no-cache, must-revalidate
  ↓ キャッシュするが常に検証

ETag を使った条件付きリクエストで、キャッシュの効率性を向上させます。

from flask import make_response
from hashlib import md5

@app.route('/api/users')
def get_users():
    data = {...}
    etag = md5(str(data).encode()).hexdigest()
    
    response = make_response(data)
    response.headers['ETag'] = etag
    response.headers['Cache-Control'] = 'public, max-age=3600'
    return response

REST API 成熟度モデル(Richardson Maturity Model)

API の設計成熟度を4段階で評価します:

レベル0: HTTP のみ

POST /api
{
  "method": "get_users",
  "id": 1
}
→ HTTP は単なる通信路、意味を持たない

レベル1: リソース指向

GET /users/1
→ リソースを個別化するが、HTTP method は使わない

レベル2: HTTP Verb の活用

GET    /users          リソース一覧
POST   /users          リソース作成
GET    /users/1        リソース詳細
PUT    /users/1        リソース更新
DELETE /users/1        リソース削除
→ HTTP method で操作の意図を表現

レベル3: HATEOAS (Hypermedia As The Engine Of Application State)

{
  "id": 1,
  "name": "Alice",
  "_links": {
    "self": { "href": "/users/1" },
    "all": { "href": "/users" },
    "edit": { "href": "/users/1", "method": "PUT" },
    "delete": { "href": "/users/1", "method": "DELETE" }
  }
}
→ クライアントが次のアクション候補をサーバーから得る

HATEOAS の利点は、API の URL が変更になっても、クライアント側の修正が最小限で済むことです。

API セキュリティ NIST ガイドライン

NIST SP 800-63-4 では、デジタル認証のベストプラクティスが定義されています。

パスワード管理 (SP 800-63B-4)

推奨:
  - 最小長: 8文字(ユーザー選択の場合)
  - 辞書チェック: 既知の侵害パスワード DB との照合
  - 定期的な変更: 不要(侵害時のみ)

非推奨:
  - 複雑性要件の強要(大文字、記号など)
  - パスワード定期変更の強制
  - セキュリティ質問による検証

多要素認証 (MFA)

レベル別実装:

Lv1: 単一要素(パスワード)
  - リスク: 侵害時に全権限喪失

Lv2: 2要素認証 (2FA)
  - 何かを知っている: パスワード
  - 何かを持っている: TOTP, SMS, ハードウェアキー
  - 相応のセキュリティ向上

Lv3: 複数要素 + 追加検証
  - 生体認証, エンタープライズ SSO 等

実装例(Python + PyOTP):

import pyotp
import qrcode
from io import BytesIO

# ユーザーが初回セットアップ時に TOTP を生成
def setup_mfa(user_id):
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret)
    
    # QR コード生成
    qr = qrcode.QRCode()
    qr.add_data(totp.provisioning_uri(name=user_id, issuer_name='MyApp'))
    img = qr.make_image()
    
    # User に QR を表示、secret をバックアップ用に保存
    return secret, img

# ログイン時に TOTP を検証
def verify_mfa(user_id, token):
    secret = db.get_user_secret(user_id)
    totp = pyotp.TOTP(secret)
    return totp.verify(token)

API Gateway パターン

複数のマイクロサービスを統一インターフェースで公開:

クライアント
    ↓
API Gateway
    ├─ 認証・認可
    ├─ Rate limiting
    ├─ キャッシング
    ├─ ロギング
    └─ ルーティング
    ↓
┌───┬─────┬────────┐
User Service
Document Service
Payment Service

実装例(FastAPI + proxy):

from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import HTTPBearer

app = FastAPI()
security = HTTPBearer()

@app.middleware("http")
async def add_process_time_header(request, call_next):
    # 認証
    if not request.headers.get("Authorization"):
        raise HTTPException(status_code=401)
    
    # Rate limiting チェック
    user_id = extract_user_id(request)
    if is_rate_limited(user_id):
        raise HTTPException(status_code=429)
    
    response = await call_next(request)
    return response

@app.get("/users/{user_id}")
async def get_user(user_id: int, token: str = Depends(security)):
    # User Service にプロキシ
    response = httpx.get(f"http://user-service/users/{user_id}")
    return response.json()

GraphQL セキュリティ考慮

GraphQL は柔軟性が高い反面、セキュリティリスクが増えやすいです。

クエリ深度の制限

# ネストが深すぎるクエリを防止
MAX_DEPTH = 5

def validate_depth(document):
    def count_depth(node, depth=0):
        if depth > MAX_DEPTH:
            raise ValueError(f"Query depth exceeds {MAX_DEPTH}")
        for field in node.get_fields():
            count_depth(field, depth + 1)
    count_depth(document)

複雑度に基づくレート制限

# 単純な行数制限ではなく、query の複雑度を計算
def estimate_query_cost(query):
    """
    各フィールドのコストを集計
    users { id }        = 1
    users { posts {} }  = 1 + 10*1 = 11
    """
    cost = 0
    for field in query.fields:
        cost += field.cost_multiplier * field.expected_count
    return cost

@graphql_app.route("/graphql", methods=["POST"])
def graphql_handler():
    query = request.json.get("query")
    cost = estimate_query_cost(query)
    if cost > MAX_COST:
        return {"error": "Query too expensive"}, 429
    return execute_query(query)

API テスト戦略

契約テスト

API クライアントとサーバー間の"契約"をテスト:

import pytest
from pact import Consumer, Provider

# クライアント側のテスト
def test_get_user():
    (Consumer('MyClient')
     .has_state('user with id 1 exists')
     .upon_receiving('a request to get user 1')
     .with_request('GET', '/users/1')
     .will_respond_with(200, body={'id': 1, 'name': 'Alice'})
     .verify())

ミューテーションテスト

エラー検出率を上げるため、API の入力を系統的に変異させ、エラー検出力を確認:

# 入力の変異例
- None
- 空文字列
- 最大値 + 1
- SQLインジェクション文字列
- XSS ペイロード

OAuth 2.0 の詳細フロー と RFC 9700 セキュリティアップデート

OAuth 2.0(RFC 6749)は、1990 年代の SAML に代わる、モダンな委譲型認証方式。RFC 9700(2024)はセキュリティ強化を加えました。

Grant Types の詳細

1. Authorization Code Flow(最も安全、推奨)

1. ユーザがアプリで「ログイン」をクリック
2. アプリがブラウザを OAuth provider(Google/GitHub等)へリダイレクト
   GET https://provider.com/oauth/authorize?client_id=...&redirect_uri=...&state=xyz
3. ユーザが provider でログイン・同意
4. Provider がコード付きでアプリにリダイレクト
   GET https://app.com/callback?code=abc&state=xyz
5. アプリがバックエンド で provider にコード交換リクエスト
   POST https://provider.com/oauth/token
   Body: code=abc, client_id=..., client_secret=...(バックエンド間通信)
6. Provider が access_token を返す
7. アプリが access_token でユーザ情報を取得
   GET https://provider.com/user?access_token=...

重要な保護

  • state パラメータで CSRF 攻撃を防止
  • client_secret はバックエンド間でのみやり取り(フロントエンドでは非公開)
  • RFC 9700 では code_challenge / code_verifier (PKCE) が必須化

2. Client Credentials(マシン間通信)

POST https://provider.com/oauth/token
Body: grant_type=client_credentials&client_id=...&client_secret=...

Response: { access_token: "...", expires_in: 3600 }

用途:

  • サーバ間通信(マイクロサービス間)
  • スケジュール済みジョブ
  • バックアップシステム

PKCE(RFC 7636)の仕組み

RFC 9700 では、シングルページアプリ(SPA)とネイティブアプリで PKCE を強制:

1. Client がランダムな code_verifier を生成(50-128 文字)
   code_verifier = "e9mVobs2iwLON4TJAE...(ランダム英数字)"

2. code_verifier を SHA256 でハッシュ化
   code_challenge = BASE64(SHA256(code_verifier))

3. Authorization リクエストに code_challenge を含める
   GET https://provider.com/oauth/authorize?
     client_id=...&code_challenge=...&code_challenge_method=S256

4. Authorization code を取得

5. Token リクエストで code_verifier を送信
   POST https://provider.com/oauth/token
   Body: code=..., code_verifier=..., client_id=...

6. Provider が code_verifier を SHA256 でハッシュ化し、
   保存した code_challenge と一致するかを検証

攻撃者が authorization code を盗んでも、code_verifier がないと token を取得できません。

NIST SP 800-63B-3(Digital Identity Guidelines)の認証要件

National Institute of Standards and Technology (NIST) が定めるパスワード・認証のガイドラインは実務標準。

パスワードポリシー(従来の誤った方針から変更)

旧来(非推奨)

  • 大文字・小文字・数字・記号の強制
  • 定期的な強制変更(90 日ごと等)

NIST 推奨(最新)

  • 長さ重視:最低 8 文字、理想は 12-16 文字
  • 一般的な脆弱パスワードのブラックリスト
    • 「123456」「password」「qwerty」等
    • サイト名・ユーザ名を含む
# パスワード検証の疑似コード
def validate_password(password, blacklist, username, site_name):
    if len(password) < 8:
        return False, "At least 8 characters"
    
    if password.lower() in blacklist:
        return False, "Password is too common"
    
    if username.lower() in password.lower():
        return False, "Password contains username"
    
    return True, "OK"

多要素認証(MFA)の要件

NIST では 3 種類を定義:

1. Something You Know(何かを知っている)

  • パスワード
  • セキュリティ質問

2. Something You Have(何かを持っている)

  • スマートフォン(SMS / TOTP)
  • ハードウェアキー(U2F / WebAuthn)
  • スマートカード

3. Something You Are(何であるか)

  • 指紋認証
  • 顔認証
  • 虹彩スキャン

NIST SP 800-63B Level 2 では、異なる 2 つのカテゴリの組み合わせを要求:

OK:パスワード + TOTP(知っている + 持っている)
OK:パスワード + 指紋(知っている + である)
NG:パスワード + セキュリティ質問(同じカテゴリ)

セッション管理(NIST 推奨)

トークンの寿命

  • Access Token:短命(15-60 分)
  • Refresh Token:長命(7-30 日)

実装:

// ログイン成功時
{
  "access_token": "eyJhbGci...",  // 短命(15分)
  "refresh_token": "refresh_xyz...",  // 長命(7日)
  "expires_in": 900  // 秒
}

// Access token が期限切れ
// Refresh token を使って新しい access token を取得
POST /oauth/token
Body: { grant_type: "refresh_token", refresh_token: "..." }

// Refresh token も期限切れ → 再ログイン

OWASP API Security Top 10(2023)

OWASP API Security Top 10 2023は、API固有の攻撃面を整理した意識向上ドキュメントです。2023版では、上位5項目のうち3つが認可に関係しており、APIでは「認証できたか」よりも「この主体が、この操作を、この対象に実行できるか」を毎回確認することが中心になります。

実務では、Top 10を脆弱性一覧として暗記するより、設計レビューの質問に変換します。

観点 レビューで問うこと
オブジェクト単位の認可 URLやbodyのIDを変えても他人の資源へ届かないか
プロパティ単位の認可 管理者用フィールドや内部メモを返していないか
機能単位の認可 一般ユーザーが管理操作を呼べないか
リソース消費 高コストAPIにrate limit、quota、timeoutがあるか
API棚卸し 古いversion、debug endpoint、未文書APIが残っていないか

API1:2023 - Broken Object Level Authorization (BOLA)

ユーザが他人のリソースにアクセスできる脆弱性。

脆弱な例

# API エンドポイント
@app.get("/api/users/{user_id}")
def get_user(user_id: int):
    user = db.query(User).filter(User.id == user_id).first()
    return user

# ユーザ 123 がログイン
GET /api/users/456  # ← ユーザ 123 が他人(456)のデータを見られる!

対策

@app.get("/api/users/{user_id}")
def get_user(user_id: int, current_user = Depends(verify_token)):
    if user_id != current_user.id:  # ← 権限チェック
        raise HTTPException(status_code=403, detail="Not authorized")
    
    user = db.query(User).filter(User.id == user_id).first()
    return user

API2:2023 - Broken Authentication

認証メカニズムの不備。

  • JWT の署名検証がない
  • デフォルトクレデンシャルが残っている
  • トークンの有効期限がない/無制限

API3:2023 - Broken Object Property Level Authorization

オブジェクト内の機密フィールドへのアクセス制御不備。

脆弱な例

GET /api/users/123
Response: {
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "password_hash": "abc123...",  // ← 公開してはいけない
  "credit_card": "4111111...",   // ← 公開してはいけない
  "salary": 100000  // ← 他人には見えてはいけない
}

対策:フロントエンドモデルとバックエンド返却データを分離

class UserPublic(BaseModel):
    id: int
    name: str
    email: str
    # password_hash, credit_card, salary は含めない

@app.get("/api/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
    user = db.query(User).filter(User.id == user_id).first()
    return user  # Pydantic が UserPublic フィールドのみ返却

API4:2023 - Unrestricted Resource Consumption

Rate limiting がなく、リソースを無制限に消費される。

攻撃例

  • 大量のリクエスト送信 → CPU 過負荷
  • 巨大なペイロード → メモリ枯渇
  • 複雑なクエリ → DB ロック

対策

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.get("/api/data")
@limiter.limit("100/minute")  # 1 分間に最大 100 リクエスト
def get_data(request: Request):
    return {"data": []}

API5:2023 - Broken Function Level Authorization

ロール別に機能をコントロールできていない。

脆弱な例

# Admin のみが削除可能な API
@app.delete("/api/users/{user_id}")
def delete_user(user_id: int):
    db.query(User).filter(User.id == user_id).delete()
    # ← role チェックがない!ユーザなら誰でも削除できる

対策

@app.delete("/api/users/{user_id}")
def delete_user(user_id: int, current_user = Depends(verify_token)):
    if current_user.role != "admin":  # ← role チェック
        raise HTTPException(status_code=403, detail="Admin only")
    
    db.query(User).filter(User.id == user_id).delete()

API6:2023 - Unrestricted Access to Sensitive Business Flows

API リクエストの順序チェックがない(例:購入前に支払い確認を取らない)。

API7:2023 - Server-Side Request Forgery (SSRF)

API が任意の URL に HTTP リクエストを送信できる。

# 脆弱
@app.get("/api/proxy")
def proxy(url: str):
    response = requests.get(url)  # ← 任意の URL にアクセス
    return response.text

# 攻撃例
GET /api/proxy?url=http://internal.db:5432  # ← 内部リソースにアクセス!

API8:2023 - Improper Assets Management

古い API バージョンが本番で動いている、ドキュメント化されていない API エンドポイントが存在等。

対策

  • API バージョン管理を厳格に
  • すべての API を登録・ドキュメント化(OpenAPI/Swagger)
  • 非推奨 API は期限付きで廃止

API9:2023 - Improper Inventory and API Versioning

API10:2023 - Unsafe Consumption of APIs

外部 API(決済、天気サービス等)のセキュリティリスク管理がない。

gRPC のセキュリティ設計

REST が JSON-based で HTTP 1.1 / 2.0 を使う一方、gRPC は Protocol Buffers(バイナリ)と HTTP/2 を使用。

TLS/SSL による暗号化

import "google.golang.org/grpc"
import "google.golang.org/grpc/credentials"

// サーバ側
lis, _ := net.Listen("tcp", ":50051")
creds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")
server := grpc.NewServer(grpc.Creds(creds))

// クライアント側
creds, _ := credentials.NewClientTLSFromFile("cert.pem", "serverName")
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))

Interceptor(ミドルウェア)による認証

func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, 
                     handler grpc.UnaryHandler) (interface{}, error) {
    // リクエストのメタデータから token を取得
    md, _ := metadata.FromIncomingContext(ctx)
    token := md.Get("authorization")
    
    if !validateToken(token) {
        return nil, status.Error(codes.Unauthenticated, "Invalid token")
    }
    
    return handler(ctx, req)
}

server := grpc.NewServer(
    grpc.UnaryInterceptor(authInterceptor),
)

まとめ

APIは、システムの接続点であり、設計の品質が後続のすべてを決めます。REST の成熟度モデル、NIST のセキュリティガイドライン、OWASP API Top 10、gRPC のセキュリティ設計、テスト戦略まで含めて、初期設計から組み込むことが重要です。

API設計と認証・認可は別のテーマですが、境界設計として強く結びついています。後から継ぎ足すより、最初から互換性、権限境界、エラー表現を含めて考える方が長期運用では安定します。

参考文献

公式・標準

解説・補助