取り組みの背景
API バックエンドのセキュリティは、プロダクション環境において最も重要な要素の一つです。認証の不備、入力バリデーションの欠如、レート制限の未設定は、データ漏洩やサービス停止といった深刻な問題を引き起こします。
対象読者: Antigravity の基本操作を理解しており、Node.js / TypeScript での API 開発経験がある中〜上級者。
前提知識・環境構築
必要な環境
- Antigravity 1.20 以降
- Node.js 20 以上
- TypeScript 5.x
- パッケージマネージャ(npm または pnpm)
プロジェクト初期化
Antigravity のエージェントに以下のプロンプトを投げることで、プロジェクトの雛形を自動生成できます。
# Antigravity エージェントへのプロンプト例
"Express + TypeScript で API サーバーのプロジェクトを作成して。
eslint, prettier, vitest を含むこと。
src/routes, src/middleware, src/validators ディレクトリを用意して。"エージェントが生成する典型的なディレクトリ構造は次の通りです。
project-root/
├── src/
│ ├── index.ts # エントリーポイント
│ ├── routes/
│ │ ├── auth.ts # 認証ルート
│ │ └── users.ts # ユーザールート
│ ├── middleware/
│ │ ├── authenticate.ts # JWT 認証ミドルウェア
│ │ ├── validate.ts # バリデーションミドルウェア
│ │ └── rateLimit.ts # レート制限ミドルウェア
│ └── validators/
│ └── schemas.ts # Zod スキーマ定義
├── tsconfig.json
└── package.json
概念と設計思想
セキュアな API バックエンドは、以下の 3 層防御モデル で設計します。
- 第 1 層 — レート制限: 過剰なリクエストをゲートウェイレベルでブロック
- 第 2 層 — 認証・認可: JWT トークンでユーザーの身元と権限を検証
- 第 3 層 — 入力バリデーション: リクエストボディ・パラメータを厳密に検証
この順序が重要です。レート制限を最初に配置することで、認証処理の負荷を軽減し、ブルートフォース攻撃を防ぎます。
Antigravity エージェントにこの設計思想を AGENTS.md に記述しておくと、後続のコード生成でも一貫したセキュリティパターンが適用されます。
# AGENTS.md(セキュリティセクション)
## API セキュリティ方針
- すべてのエンドポイントにレート制限を適用する
- 認証が必要なルートには JWT ミドルウェアを必ず挟む
- リクエストボディは Zod スキーマで検証してからビジネスロジックに渡す
- エラーレスポンスには内部情報を含めないステップバイステップ実装
Step 1: JWT 認証ミドルウェア
まず、JWT の発行と検証を行う認証ミドルウェアを実装します。
// src/middleware/authenticate.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
interface TokenPayload {
userId: string;
role: "user" | "admin";
iat: number;
exp: number;
}
// アクセストークン検証ミドルウェア
export function authenticate(
req: Request,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({
error: "UNAUTHORIZED",
message: "認証トークンが必要です",
});
return;
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as TokenPayload;
// req オブジェクトにユーザー情報を付与
req.user = { userId: payload.userId, role: payload.role };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({
error: "TOKEN_EXPIRED",
message: "トークンの有効期限が切れています",
});
return;
}
res.status(401).json({
error: "INVALID_TOKEN",
message: "無効なトークンです",
});
}
}
// ロールベースの認可ミドルウェア
export function authorize(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
res.status(403).json({
error: "FORBIDDEN",
message: "この操作を行う権限がありません",
});
return;
}
next();
};
}Antigravity エージェントへのプロンプト: 「リフレッシュトークンのローテーション機能を追加して。Redis に保存するパターンで。」 と指示すると、エージェントがトークンローテーションロジックを自動生成します。
Step 2: Zod によるスキーマバリデーション
入力バリデーションには Zod を使い、型安全かつ宣言的にスキーマを定義します。
// src/validators/schemas.ts
import { z } from "zod";
// ユーザー登録スキーマ
export const registerSchema = z.object({
body: z.object({
email: z
.string()
.email("有効なメールアドレスを入力してください")
.max(255),
password: z
.string()
.min(8, "パスワードは8文字以上です")
.max(128)
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"英大文字・英小文字・数字をそれぞれ1文字以上含めてください"
),
name: z.string().min(1).max(100).trim(),
}),
});
// ページネーションスキーマ(クエリパラメータ用)
export const paginationSchema = z.object({
query: z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(["createdAt", "updatedAt", "name"]).default("createdAt"),
order: z.enum(["asc", "desc"]).default("desc"),
}),
});
// 汎用バリデーションミドルウェア
// src/middleware/validate.ts
import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
export function validate(schema: AnyZodObject) {
return async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (err) {
if (err instanceof ZodError) {
const errors = err.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
res.status(400).json({
error: "VALIDATION_ERROR",
details: errors,
});
return;
}
next(err);
}
};
}Step 3: トークンバケット方式のレート制限
IP アドレスごとにリクエスト数を制限するレート制限ミドルウェアを実装します。本番環境では Redis を使いますが、ここではインメモリ版とRedis 版の両方を示します。
// src/middleware/rateLimit.ts
import { Request, Response, NextFunction } from "express";
interface BucketEntry {
tokens: number;
lastRefill: number;
}
// インメモリ版トークンバケット(開発・小規模向け)
class TokenBucket {
private buckets = new Map<string, BucketEntry>();
private maxTokens: number;
private refillRate: number; // トークン/秒
constructor(maxTokens: number, refillRate: number) {
this.maxTokens = maxTokens;
this.refillRate = refillRate;
// 古いエントリを定期的にクリーンアップ
setInterval(() => this.cleanup(), 60_000);
}
consume(key: string): { allowed: boolean; remaining: number; retryAfter?: number } {
const now = Date.now();
let bucket = this.buckets.get(key);
if (!bucket) {
bucket = { tokens: this.maxTokens - 1, lastRefill: now };
this.buckets.set(key, bucket);
return { allowed: true, remaining: bucket.tokens };
}
// トークンを補充
const elapsed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(
this.maxTokens,
bucket.tokens + elapsed * this.refillRate
);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
const retryAfter = Math.ceil((1 - bucket.tokens) / this.refillRate);
return { allowed: false, remaining: 0, retryAfter };
}
bucket.tokens -= 1;
return { allowed: true, remaining: Math.floor(bucket.tokens) };
}
private cleanup(): void {
const cutoff = Date.now() - 300_000; // 5 分以上前のエントリを削除
for (const [key, entry] of this.buckets) {
if (entry.lastRefill < cutoff) this.buckets.delete(key);
}
}
}
// エンドポイント別にバケットを作成
const generalBucket = new TokenBucket(60, 1); // 60 req/分
const authBucket = new TokenBucket(10, 0.167); // 10 req/分(ブルートフォース防止)
export function rateLimit(type: "general" | "auth" = "general") {
const bucket = type === "auth" ? authBucket : generalBucket;
return (req: Request, res: Response, next: NextFunction): void => {
const key = req.ip || req.socket.remoteAddress || "unknown";
const result = bucket.consume(key);
res.setHeader("X-RateLimit-Remaining", result.remaining.toString());
if (!result.allowed) {
res.setHeader("Retry-After", result.retryAfter!.toString());
res.status(429).json({
error: "RATE_LIMITED",
message: "リクエスト数の上限に達しました。しばらく待ってからお試しください。",
retryAfter: result.retryAfter,
});
return;
}
next();
};
}Step 4: ルートへの統合
3 つのミドルウェアをルートに統合し、3 層防御を実現します。
// src/routes/users.ts
import { Router } from "express";
import { authenticate, authorize } from "../middleware/authenticate";
import { validate } from "../middleware/validate";
import { rateLimit } from "../middleware/rateLimit";
import { paginationSchema } from "../validators/schemas";
const router = Router();
// GET /users — 管理者のみ、レート制限付き
router.get(
"/",
rateLimit("general"), // 第 1 層: レート制限
authenticate, // 第 2 層: 認証
authorize("admin"), // 第 2 層: 認可
validate(paginationSchema), // 第 3 層: バリデーション
async (req, res) => {
const { page, limit, sort, order } = req.query as any;
// ビジネスロジック — DB クエリ等
res.json({ users: [], total: 0, page, limit });
}
);
export default router;応用パターン
パターン 1: エンドポイント別のレート制限戦略
認証系エンドポイント(ログイン、パスワードリセット)には厳しいレート制限を適用し、公開 API には緩めの制限を設定するのがベストプラクティスです。
// 認証ルートには厳しい制限
app.use("/api/auth", rateLimit("auth")); // 10 req/分
// 一般ルートには通常の制限
app.use("/api/v1", rateLimit("general")); // 60 req/分パターン 2: Antigravity エージェントによるセキュリティ監査
Antigravity の Manager Surface を使えば、専用のセキュリティ監査エージェントを設定できます。
# AGENTS.md — セキュリティ監査エージェント
## security-auditor
あなたはセキュリティ専門のレビュアーです。以下を重点的にチェックしてください:
- SQL インジェクション・XSS の脆弱性
- 認証バイパスの可能性
- 環境変数のハードコーディング
- エラーメッセージへの内部情報の露出
- CORS 設定の適切性パターン 3: Helmet と CORS の設定
Express のセキュリティヘッダー設定は Helmet で自動化できます。
import helmet from "helmet";
import cors from "cors";
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || [],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
}));トラブルシューティング
JWT トークンが常に無効と判定される
原因として多いのは、環境変数 JWT_SECRET が設定されていないケースです。.env ファイルと dotenv の読み込み順序を確認してください。また、トークンの発行時と検証時で異なるシークレットを使っていないかもチェックしましょう。
レート制限が効かない(常にリセットされる)
インメモリ版のトークンバケットは、サーバーの再起動でリセットされます。開発中に nodemon で頻繁にリスタートしている場合は正常な動作です。本番環境では Redis ベースの実装に切り替えてください。
Zod のバリデーションエラーが詳細すぎる
プロダクション環境では、バリデーションエラーの詳細をそのままクライアントに返すとセキュリティリスクになります。環境変数で出力レベルを制御しましょう。
const isDev = process.env.NODE_ENV === "development";
// 本番ではフィールド名のみ返し、詳細メッセージは省略
const errors = err.errors.map((e) => ({
field: e.path.join("."),
...(isDev && { message: e.message }),
}));パフォーマンス考慮事項
- JWT の検証コスト:
HS256は軽量ですが、マイクロサービス間ではRS256(公開鍵方式)を使うと、各サービスが秘密鍵を持たずに検証できます。 - レート制限のストレージ: 小規模なら
Mapで十分ですが、複数プロセス / 複数サーバー構成では Redis を使ってください。Antigravity エージェントに「Redis ベースのレート制限に切り替えて」と指示すれば、ioredisを使った実装を自動生成できます。 - Zod のパフォーマンス: 大量のリクエストを処理する場合、Zod のパース処理がボトルネックになることがあります。
z.preprocessでコストの高い変換を避け、シンプルなスキーマを維持しましょう。
まとめ
Antigravity の AI エージェントを活用することで、セキュアな API バックエンドの構築を大幅に加速できます。本記事で紹介した 3 層防御モデル — レート制限、JWT 認証、Zod バリデーション — を組み合わせることで、プロダクションレベルのセキュリティを実現できます。
重要なのは、エージェントが生成したコードを盲信せず、セキュリティ観点でのレビューを欠かさないことです。AGENTS.md にセキュリティ方針を明記し、Manager Surface でセキュリティ監査エージェントを設定することで、開発プロセス全体のセキュリティレベルを底上げできます。
次のステップとして、Antigravity + Cloudflare Workers でエッジ API を構築するや、Antigravity + Prisma ORM でデータベース層を設計するも参考にしてください。また、Antigravity の AI コードレビュー機能を活用して、セキュリティレビューを自動化することもおすすめです。