メインコンテンツへスキップ
#Web制作 約6分で読めます

Astro Middleware で認証・認可を実装する方法|JWT・セッション・OAuth完全ガイド【2026年4月】

Astro MiddlewareでJWT・セッション・OAuth2.0を使った認証を実装する手順を解説。Astro 5.2のMiddleware型安全性強化とcontext.locals改善を活用した実装例を紹介

Astroで認証機能を実装する際、Middlewareを使った実装が主流になっています。本記事では、Astro MiddlewareでJWT認証・セッションベース認証・OAuth2.0を実装する具体的な手順と、最新のベストプラクティスを解説します。

Astro Middleware とは|認証実装に最適な理由

Astro Middlewareは、リクエストとレスポンスの間で実行される関数で、認証チェック・ログ記録・ヘッダー追加などの共通処理を一元管理できます。

Middlewareの基本構造

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  // リクエスト前処理
  console.log(`Request: ${context.url.pathname}`);
  
  // 次のハンドラーへ
  const response = await next();
  
  // レスポンス後処理
  return response;
});

認証実装に最適な3つの理由

  1. 全ルートで一元管理: /api/*/dashboard/* など特定パスに対して認証チェックを適用
  2. context.locals で状態共有: 認証情報をページコンポーネントやAPIエンドポイントに渡せる
  3. SSR・SSG両対応: ハイブリッドレンダリングでも動作
flowchart LR
    A[クライアント] --> B[Middleware]
    B --> C{認証チェック}
    C -->|成功| D[ページレンダリング]
    C -->|失敗| E[401/リダイレクト]
    D --> F[レスポンス]
    E --> F

Astro Middlewareの実行フロー: リクエストごとに認証チェックを挟み、context.localsで認証情報を共有

JWT認証の実装|トークンベース認証の基本

JWTはステートレスな認証方式で、APIサーバーとの連携に適しています。

JWT検証Middlewareの実装

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  import.meta.env.JWT_SECRET
);

export const onRequest = defineMiddleware(async (context, next) => {
  // 公開ページは認証不要
  if (context.url.pathname.startsWith('/login')) {
    return next();
  }

  // Cookieからトークン取得
  const token = context.cookies.get('auth_token')?.value;

  if (!token) {
    // 未認証時はログインページへリダイレクト
    return context.redirect('/login');
  }

  try {
    // JWT検証
    const { payload } = await jwtVerify(token, JWT_SECRET);
    
    // context.localsにユーザー情報を保存
    context.locals.user = {
      id: payload.sub as string,
      email: payload.email as string,
      role: payload.role as string,
    };

    return next();
  } catch (error) {
    // トークン無効時は認証エラー
    console.error('JWT verification failed:', error);
    return new Response('Unauthorized', { status: 401 });
  }
});

ログインAPIエンドポイントの実装

// src/pages/api/login.ts
import type { APIRoute } from 'astro';
import { SignJWT } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  import.meta.env.JWT_SECRET
);

export const POST: APIRoute = async ({ request, cookies }) => {
  const { email, password } = await request.json();

  // パスワード検証(実際はハッシュ化して比較)
  const user = await verifyCredentials(email, password);
  if (!user) {
    return new Response('Invalid credentials', { status: 401 });
  }

  // JWT生成
  const token = await new SignJWT({
    sub: user.id,
    email: user.email,
    role: user.role,
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .sign(JWT_SECRET);

  // HttpOnly Cookieにセット
  cookies.set('auth_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7, // 7日間
    path: '/',
  });

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
  });
};

ページでユーザー情報を取得

---
// src/pages/dashboard.astro
const user = Astro.locals.user;

if (!user) {
  return Astro.redirect('/login');
}
---

<html>
  <body>
    <h1>Welcome, {user.email}</h1>
    <p>Your role: {user.role}</p>
  </body>
</html>

セッションベース認証の実装|Redis・KVストアとの連携

セッションベース認証は、サーバー側でセッション情報を管理する従来型の方式です。

Redis セッションストアの実装

// src/lib/session.ts
import { createClient } from 'redis';

const redis = createClient({
  url: import.meta.env.REDIS_URL,
});
await redis.connect();

export async function createSession(userId: string): Promise<string> {
  const sessionId = crypto.randomUUID();
  
  // セッションデータをRedisに保存(有効期限30分)
  await redis.set(
    `session:${sessionId}`,
    JSON.stringify({ userId, createdAt: Date.now() }),
    { EX: 1800 }
  );

  return sessionId;
}

export async function getSession(sessionId: string) {
  const data = await redis.get(`session:${sessionId}`);
  return data ? JSON.parse(data) : null;
}

export async function deleteSession(sessionId: string) {
  await redis.del(`session:${sessionId}`);
}

セッション検証Middleware

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { getSession } from './lib/session';

export const onRequest = defineMiddleware(async (context, next) => {
  const sessionId = context.cookies.get('session_id')?.value;

  if (!sessionId) {
    return context.redirect('/login');
  }

  const session = await getSession(sessionId);
  if (!session) {
    // セッション期限切れ
    return context.redirect('/login');
  }

  // ユーザー情報をDBから取得
  const user = await getUserById(session.userId);
  context.locals.user = user;

  return next();
});

セッションベース vs JWT|選択基準

項目セッションベースJWT
サーバー状態ステートフル(Redis等が必要)ステートレス
スケーラビリティRedisクラスタが必要高い(サーバー側で状態不要)
セキュリティサーバー側で即座に無効化可能トークン期限まで無効化不可
適用場面管理画面・高セキュリティ要件API認証・マイクロサービス

OAuth2.0 認証の実装|Google・GitHub ログイン

OAuth2.0は外部プロバイダー(Google、GitHub等)を使った認証方式です。

OAuth フロー図

sequenceDiagram
    participant U as ユーザー
    participant A as Astroアプリ
    participant G as Google OAuth
    participant DB as データベース

    U->>A: ログインボタンクリック
    A->>G: 認証リクエスト(client_id + redirect_uri)
    G->>U: Googleログイン画面
    U->>G: 認証・権限承認
    G->>A: 認可コード返却(callback)
    A->>G: アクセストークン要求(code + client_secret)
    G->>A: アクセストークン発行
    A->>G: ユーザー情報取得
    G->>A: プロフィール情報
    A->>DB: ユーザー登録/更新
    A->>U: セッション発行・リダイレクト

OAuth2.0の認証フロー: 認可コードを使った安全なトークン取得プロセス

Google OAuth 実装例

// src/pages/api/auth/google.ts
import type { APIRoute } from 'astro';

const GOOGLE_CLIENT_ID = import.meta.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = import.meta.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'https://example.com/api/auth/google/callback';

export const GET: APIRoute = async () => {
  const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  authUrl.searchParams.set('client_id', GOOGLE_CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', 'openid email profile');

  return Response.redirect(authUrl.toString());
};

コールバック処理とユーザー情報取得

// src/pages/api/auth/google/callback.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = async ({ url, cookies }) => {
  const code = url.searchParams.get('code');
  
  if (!code) {
    return new Response('Authorization code missing', { status: 400 });
  }

  // アクセストークン取得
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: import.meta.env.GOOGLE_CLIENT_ID,
      client_secret: import.meta.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: 'https://example.com/api/auth/google/callback',
      grant_type: 'authorization_code',
    }),
  });

  const { access_token } = await tokenResponse.json();

  // ユーザー情報取得
  const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${access_token}` },
  });

  const userData = await userResponse.json();

  // DBにユーザー登録/更新
  const user = await upsertUser({
    email: userData.email,
    name: userData.name,
    picture: userData.picture,
  });

  // セッション発行
  const sessionId = await createSession(user.id);
  cookies.set('session_id', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  });

  return Response.redirect('/dashboard');
};

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

認証後、ユーザーの権限に応じたアクセス制御を実装します。

RBAC Middleware

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { sequence } from 'astro:middleware';

const authMiddleware = defineMiddleware(async (context, next) => {
  // JWT検証(前述の実装)
  // context.locals.user にユーザー情報を保存
  return next();
});

const rbacMiddleware = defineMiddleware(async (context, next) => {
  const user = context.locals.user;

  // 管理者専用ページのアクセス制御
  if (context.url.pathname.startsWith('/admin')) {
    if (user?.role !== 'admin') {
      return new Response('Forbidden', { status: 403 });
    }
  }

  return next();
});

// Middlewareをチェーン
export const onRequest = sequence(authMiddleware, rbacMiddleware);

パーミッションチェック関数

// src/lib/permissions.ts
type Permission = 'read' | 'write' | 'delete' | 'admin';

const rolePermissions: Record<string, Permission[]> = {
  admin: ['read', 'write', 'delete', 'admin'],
  editor: ['read', 'write'],
  viewer: ['read'],
};

export function hasPermission(role: string, permission: Permission): boolean {
  return rolePermissions[role]?.includes(permission) ?? false;
}

ページでの権限チェック

---
// src/pages/admin/users.astro
import { hasPermission } from '@/lib/permissions';

const user = Astro.locals.user;

if (!user || !hasPermission(user.role, 'admin')) {
  return Astro.redirect('/unauthorized');
}
---

<html>
  <body>
    <h1>User Management</h1>
    <!-- 管理者専用コンテンツ -->
  </body>
</html>

セキュリティベストプラクティス

CSRF対策

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  if (context.request.method !== 'GET') {
    const csrfToken = context.request.headers.get('X-CSRF-Token');
    const sessionCsrfToken = context.cookies.get('csrf_token')?.value;

    if (!csrfToken || csrfToken !== sessionCsrfToken) {
      return new Response('CSRF token mismatch', { status: 403 });
    }
  }

  return next();
});

レート制限の実装

// src/lib/rate-limit.ts
import { createClient } from 'redis';

const redis = createClient({ url: import.meta.env.REDIS_URL });
await redis.connect();

export async function checkRateLimit(
  ip: string,
  limit: number = 100,
  window: number = 3600
): Promise<boolean> {
  const key = `rate_limit:${ip}`;
  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, window);
  }

  return current <= limit;
}
// src/middleware.ts
import { checkRateLimit } from './lib/rate-limit';

export const onRequest = defineMiddleware(async (context, next) => {
  const ip = context.clientAddress;

  if (!(await checkRateLimit(ip))) {
    return new Response('Too Many Requests', { status: 429 });
  }

  return next();
});

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

// src/middleware.ts
export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next();

  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-XSS-Protection', '1; mode=block');
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  );
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  );

  return response;
});

まとめ

Astro Middlewareを使った認証実装の要点:

  • JWT認証: ステートレスでスケーラブル、API認証に最適
  • セッションベース認証: サーバー側で厳密に管理、即座に無効化可能
  • OAuth2.0: 外部プロバイダーを活用、実装コストを削減
  • RBAC: ロールベースでアクセス制御を一元管理
  • セキュリティ対策: CSRF・レート制限・セキュリティヘッダーを必ず実装

認証方式の選択は、アプリケーションの要件・スケール・セキュリティポリシーに応じて決定してください。

参考リンク

#Astro #Middleware #認証 #セキュリティ #JWT
シェア