近年、APIを標的としたサイバー攻撃は急増しています。OWASP API Security Top 10によると、認証の不備、認可の欠陥、レート制限の不足が上位に挙げられており、これらは多くのインシデントの直接的な原因となっています。本記事では、APIセキュリティの三本柱である認証・認可・レート制限について、設計原則から実装まで体系的に解説します。
筆者はこれまで複数のBtoB SaaSのAPI設計に携わってきましたが、セキュリティは後付けではなく設計段階から組み込むべきものだと痛感しています。一度公開されたAPIのセキュリティモデルを変更するのは、想像以上にコストがかかるからです。
現代の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)
}
認可は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ヘッダーによる防御も重要です。以下の設定を基本として適用しましょう。
APIセキュリティは一度設計して終わりではなく、継続的に見直すべきものです。定期的なセキュリティ監査と、OWASP API Security Top 10の最新版を参照しながら、常に防御を強化していくことが重要です。