一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • APIセキュリティの設計原則:認証・認可・レート制限の実装

APIセキュリティが重要な理由

近年、APIを標的としたサイバー攻撃は急増しています。OWASP API Security Top 10によると、認証の不備、認可の欠陥、レート制限の不足が上位に挙げられており、これらは多くのインシデントの直接的な原因となっています。本記事では、APIセキュリティの三本柱である認証・認可・レート制限について、設計原則から実装まで体系的に解説します。

筆者はこれまで複数のBtoB SaaSのAPI設計に携わってきましたが、セキュリティは後付けではなく設計段階から組み込むべきものだと痛感しています。一度公開されたAPIのセキュリティモデルを変更するのは、想像以上にコストがかかるからです。

認証の設計と実装

JWT認証の基本構成

現代のAPI認証において、JWTはデファクトスタンダードです。ただし、JWTの運用には注意すべきポイントが多数あります。以下にNode.js(Express)での基本的な実装を示します。

import jwt from 'jsonwebtoken'
import { Request, Response, NextFunction } from 'express'

const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!

interface TokenPayload {
userId: string
roles: string[]
iat: number
exp: number
}

export function generateTokenPair(userId: string, roles: string[]) {
const accessToken = jwt.sign(
{ userId, roles },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m', algorithm: 'RS256' }
)
const refreshToken = jwt.sign(
{ userId },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d', algorithm: 'RS256' }
)
return { accessToken, refreshToken }
}

export function authenticateToken(
req: Request, res: Response, next: NextFunction
) {
const authHeader = req.headers['authorization']
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: null

if (!token) {
return res.status(401).json({ error: 'Access token required' })
}

try {
const payload = jwt.verify(token, ACCESS_TOKEN_SECRET) as TokenPayload
req.user = payload
next()
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired token' })
}
}

ここで重要なのは、algorithm: 'RS256'を明示的に指定している点です。アルゴリズムを指定しないと、「none」アルゴリズム攻撃やHS256/RS256混同攻撃の対象となる可能性があります。また、アクセストークンの有効期限を15分と短く設定し、リフレッシュトークンとの二段構成にすることで、トークン漏洩時のリスクを最小化しています。

リフレッシュトークンのローテーション

リフレッシュトークンは長期間有効なため、盗難対策としてローテーションを実装すべきです。トークンが使用されるたびに新しいリフレッシュトークンを発行し、古いものを無効化する仕組みです。

export async function rotateRefreshToken(oldRefreshToken: string) {
const payload = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET) as TokenPayload

const isRevoked = await tokenStore.isRevoked(oldRefreshToken)
if (isRevoked) {
await tokenStore.revokeAllForUser(payload.userId)
throw new Error('Refresh token reuse detected')
}

await tokenStore.revoke(oldRefreshToken)

const user = await userRepository.findById(payload.userId)
return generateTokenPair(user.id, user.roles)
}

認可の設計パターン

RBAC(ロールベースアクセス制御)の実装

認可はAPIセキュリティにおいて最も設計が難しい領域です。RBACは最も広く採用されているモデルで、以下のようにミドルウェアとして実装できます。

type Role = 'admin' | 'editor' | 'viewer'

const permissions: Record = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
}

export function authorize(...requiredPermissions: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user?.roles as Role[]
if (!userRoles) {
return res.status(403).json({ error: 'No roles assigned' })
}

const userPermissions = userRoles.flatMap(role => permissions[role])
const hasPermission = requiredPermissions.every(
perm => userPermissions.includes(perm)
)

if (!hasPermission) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
next()
}
}

app.delete('/api/posts/:id',
authenticateToken,
authorize('delete'),
deletePostHandler
)

実務での教訓として、認可チェックは必ずサーバーサイドで行うことが鉄則です。クライアント側のUIでボタンを非表示にするだけでは、APIを直接叩かれた場合に無防備になります。

レート制限の実装戦略

レート制限は、DoS攻撃の防御だけでなく、API利用の公平性を保つためにも不可欠です。Redisを使ったスライディングウィンドウ方式の実装を示します。

import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

export function rateLimit(windowMs: number, maxRequests: number) {
return async (req: Request, res: Response, next: NextFunction) => {
const key = `rate:${req.user?.userId || req.ip}`
const now = Date.now()
const windowStart = now - windowMs

const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, 0, windowStart)
pipeline.zadd(key, now.toString(), `${now}-${Math.random()}`)
pipeline.zcard(key)
pipeline.expire(key, Math.ceil(windowMs / 1000))

const results = await pipeline.exec()
const requestCount = results![2][1] as number

res.setHeader('X-RateLimit-Limit', maxRequests)
res.setHeader('X-RateLimit-Remaining', Math.max(0, maxRequests - requestCount))

if (requestCount > maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(windowMs / 1000),
})
}
next()
}
}

app.use('/api/', rateLimit(60 * 1000, 100))

レート制限のレスポンスヘッダーに残りリクエスト数を含めることで、クライアント側が適切にリクエストを調整できるようになります。これはAPI利用者にとって非常に有用な情報です。

セキュリティヘッダーの追加設定

認証・認可・レート制限に加えて、HTTPヘッダーによる防御も重要です。以下の設定を基本として適用しましょう。

  • CORS設定:許可するオリジンを明示的に指定する(ワイルドカードは避ける)
  • Content-Type検証:リクエストのContent-Typeを厳密にチェックする
  • 入力値バリデーション:zodやjoiなどのスキーマバリデーションを全エンドポイントに適用する
  • エラーメッセージの制御:スタックトレースや内部情報を公開しない

APIセキュリティは一度設計して終わりではなく、継続的に見直すべきものです。定期的なセキュリティ監査と、OWASP API Security Top 10の最新版を参照しながら、常に防御を強化していくことが重要です。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア