Use Cases (更新: 2026/6/1)

Claude Codeで安全なパスワードリセットを実装する方法

Claude Codeでパスワードリセットを安全に実装する手順。トークン、rate limit、監査ログ、MFAとの分離まで解説します。

Claude Codeで安全なパスワードリセットを実装する方法

パスワードリセットは「ログインできない人を助ける機能」に見えますが、実態はアカウントを取り戻すための別ルートです。ここを雑に作ると、ログイン画面をどれだけ固めても、メールアドレスの存在確認、トークン漏えい、再利用、セッション乗っ取りの入口になります。

Claude Codeに任せる価値があるのは、フォームだけではありません。DBスキーマ、ランダムトークン、一回使用、短い有効期限、rate limit、メール文面、Referrer-Policynoindex、セッション無効化、監査ログまでを同じ差分でそろえられる点です。ただし、指示の粒度が粗いと「動くが危ないリセット機能」ができやすいので、この記事ではMasaが実装レビューで実際に見る観点に寄せて、Claude Codeへ渡せる単位まで分解します。

根拠としては、OWASPの Forgot Password Cheat SheetAuthentication Cheat SheetTesting for Weak Password Change or Reset Functionalities をベースにしています。認証全体の考え方は Claude CodeセキュリティベストプラクティスClaude Code環境変数管理 も合わせて読むとつながります。

安全なリセットフローの全体像

まず、この記事で作るフローを図にします。トークンは「一時的な合鍵」です。DBには合鍵そのものを保存せず、ハッシュ化した値だけを保存します。監査ログは「後から追える操作記録」です。問い合わせ対応や不正検知で必要になります。

flowchart TD
  A[User submits email] --> B[Return the same message for every email]
  B --> C{Rate limit ok?}
  C -- no --> Z[Do not reveal whether account exists]
  C -- yes --> D{User exists?}
  D -- no --> Z
  D -- yes --> E[Generate random token with Node crypto]
  E --> F[Store only SHA-256 token hash]
  F --> G[Send HTTPS reset link by email]
  G --> H[Reset page with noindex and Referrer-Policy]
  H --> I[User submits new password]
  I --> J[Claim token once in DB transaction]
  J --> K[Hash password and revoke sessions]
  K --> L[Write audit log and ask user to sign in]

重要な設計判断は次の通りです。

観点採用する方針理由
ユーザー存在確認存在しても、しなくても同じレスポンスメールアドレス列挙を防ぐ
トークンcrypto.randomBytes(32).toString("base64url")推測困難なランダム値にする
保存方法トークン本体ではなくSHA-256ハッシュを保存DB漏えい時にURLトークンを使わせない
有効期限30分メール遅延とリスクのバランスを取る
使用回数1回だけ転送・履歴・ログ経由の再利用を防ぐ
rate limitIPとメールアドレス単位大量メール送信とトークン総当たりを抑える
メールパスワードを絶対に書かないメールは安全な保管場所ではない
リセット画面noindexReferrer-Policy: no-referrer検索登録と外部サイトへのトークン漏れを避ける
リセット後既存セッションを無効化し、自動ログインしない乗っ取られたセッションを残さない
MFAパスワードリセットではMFAを解除しないMFA再設定は別の本人確認フローにする

Claude Codeに任せる作業粒度

Claude Codeには「パスワードリセットを作って」ではなく、レビューできる小さな単位で依頼します。セキュリティ機能は、差分が大きすぎると見逃しが増えるからです。

おすすめの分け方は次の5つです。

  1. Prisma schemaに PasswordResetTokenSessionAuditLog を追加する。
  2. Node cryptoでトークンを作り、ハッシュ保存するサービス層を作る。
  3. Next.js App Routerの requestconfirm API routeを作る。
  4. メールテンプレート、Referrer-PolicyX-Robots-Tag、resetページを作る。
  5. ユーザー存在漏えい、1回使用、期限切れ、session revokeのテストを追加する。

Claude Codeへ渡すプロンプトは、たとえば次のようにします。

Next.js App Router + TypeScript + Prismaでパスワードリセットを実装してください。
制約:
- 存在するメールでも存在しないメールでも同じJSONと近い応答時間にする
- reset tokenはNode cryptoで32 bytes以上を生成し、DBにはSHA-256 hashだけ保存する
- tokenは30分で期限切れ、1回使用、使用済みは再利用不可
- request APIはIPとemail単位でrate limitする
- reset pageはnoindex、Referrer-Policy no-referrerを付ける
- reset成功後は全既存sessionをrevokeし、自動ログインしない
- MFA resetは実装しない。MFA再設定が必要なら別フローに分ける
- tokenやpasswordをログに出さない。audit logにはevent, userId, ip, userAgentだけ残す
対象ファイルだけを変更し、schema変更・API・UI・テストを分けて説明してください。

ここまで具体化しておくと、Claude Codeの出力をレビューするときも「指示通りか」を機械的に確認できます。

Prismaスキーマを用意する

以下は、この記事のコードが前提にする最小スキーマです。既存アプリに UserSession がある場合は、名前を合わせて取り込んでください。

// prisma/schema.prisma
model User {
  id                String               @id @default(cuid())
  email             String               @unique
  passwordHash      String
  mfaEnabled        Boolean              @default(false)
  passwordChangedAt DateTime?
  createdAt         DateTime             @default(now())
  resetTokens       PasswordResetToken[]
  sessions          Session[]
  auditLogs         AuditLog[]
}

model PasswordResetToken {
  id                 String    @id @default(cuid())
  userId             String
  tokenHash          String    @unique
  expiresAt          DateTime
  usedAt             DateTime?
  createdAt          DateTime  @default(now())
  requestedIp        String?
  requestedUserAgent String?
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, expiresAt])
  @@index([createdAt])
}

model Session {
  id        String    @id @default(cuid())
  userId    String
  revokedAt DateTime?
  createdAt DateTime  @default(now())
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, revokedAt])
}

model AuditLog {
  id        String   @id @default(cuid())
  userId    String?
  event     String
  ip        String?
  userAgent String?
  metadata  Json?
  createdAt DateTime @default(now())
  user      User?    @relation(fields: [userId], references: [id], onDelete: SetNull)

  @@index([userId, createdAt])
  @@index([event, createdAt])
}

ポイントは tokenHash に一意制約を付けることです。トークン本体はメールにだけ載せ、DBには保存しません。requestedIprequestedUserAgent は不正利用の調査用です。生のトークン、送信されたパスワード、メール本文全体は監査ログに残しません。

トークン生成、rate limit、メール送信を実装する

次のサービス層は、そのままコピーして小さなNext.js/Prismaアプリで動かせる粒度にしています。依存は @prisma/clientbcryptjszod です。メールはResend APIを例にしていますが、RESEND_API_KEY がないローカルではコンソールにURLを出すだけにしています。

// src/lib/password-reset.ts
import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const RESET_TTL_MINUTES = 30;
const TOKEN_BYTES = 32;
const MIN_RESPONSE_MS = 450;
const REQUEST_RESPONSE = {
  message: "If an account exists for that email, a password reset link has been sent.",
};

type ResetRequestInput = {
  email: string;
  ip?: string;
  userAgent?: string;
};

type ResetPasswordInput = {
  token: string;
  newPassword: string;
  ip?: string;
  userAgent?: string;
};

const memoryLimits = new Map<string, { count: number; resetAt: number }>();

function normalizeEmail(email: string) {
  return email.trim().toLowerCase();
}

function hashToken(token: string) {
  return crypto.createHash("sha256").update(token, "utf8").digest("hex");
}

function appOrigin() {
  const origin = process.env.APP_ORIGIN;
  if (!origin || !origin.startsWith("https://")) {
    throw new Error("APP_ORIGIN must be an HTTPS origin such as https://example.com");
  }
  return origin.replace(/\/$/, "");
}

function isLimited(key: string, max: number, windowMs: number) {
  const now = Date.now();
  const current = memoryLimits.get(key);

  if (!current || current.resetAt <= now) {
    memoryLimits.set(key, { count: 1, resetAt: now + windowMs });
    return false;
  }

  current.count += 1;
  return current.count > max;
}

async function padResponse(startedAt: number) {
  const elapsed = Date.now() - startedAt;
  if (elapsed < MIN_RESPONSE_MS) {
    await new Promise((resolve) => setTimeout(resolve, MIN_RESPONSE_MS - elapsed));
  }
}

async function sendPasswordResetEmail(input: {
  to: string;
  resetUrl: string;
  expiresMinutes: number;
}) {
  const subject = "Reset your password";
  const html = `
    <div style="font-family:Arial,sans-serif;line-height:1.6;color:#111827">
      <h1 style="font-size:20px">Reset your password</h1>
      <p>Use the button below to set a new password. This link expires in ${input.expiresMinutes} minutes.</p>
      <p><a href="${input.resetUrl}" style="display:inline-block;background:#2563eb;color:#ffffff;padding:12px 18px;border-radius:6px;text-decoration:none">Reset password</a></p>
      <p>If you did not request this email, you can ignore it. Your password has not been changed.</p>
    </div>
  `;

  if (!process.env.RESEND_API_KEY) {
    console.info("Password reset email skipped in local development", {
      to: input.to,
      resetUrl: input.resetUrl,
    });
    return;
  }

  const response = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: process.env.MAIL_FROM ?? "support@example.com",
      to: input.to,
      subject,
      html,
    }),
  });

  if (!response.ok) {
    throw new Error(`Failed to send reset email: ${response.status}`);
  }
}

export async function requestPasswordReset(input: ResetRequestInput) {
  const startedAt = Date.now();
  const email = normalizeEmail(input.email);
  const ip = input.ip ?? "unknown";
  const userAgent = input.userAgent?.slice(0, 300);

  const limited =
    isLimited(`password-reset:ip:${ip}`, 20, 60 * 60 * 1000) ||
    isLimited(`password-reset:email:${email}`, 5, 15 * 60 * 1000);

  try {
    if (limited) {
      await padResponse(startedAt);
      return REQUEST_RESPONSE;
    }

    const user = await prisma.user.findUnique({
      where: { email },
      select: { id: true, email: true },
    });

    if (user) {
      const token = crypto.randomBytes(TOKEN_BYTES).toString("base64url");
      const tokenHash = hashToken(token);
      const expiresAt = new Date(Date.now() + RESET_TTL_MINUTES * 60 * 1000);

      await prisma.passwordResetToken.create({
        data: {
          userId: user.id,
          tokenHash,
          expiresAt,
          requestedIp: ip,
          requestedUserAgent: userAgent,
        },
      });

      await prisma.auditLog.create({
        data: {
          userId: user.id,
          event: "password_reset_requested",
          ip,
          userAgent,
          metadata: { expiresAt: expiresAt.toISOString() },
        },
      });

      const resetUrl = `${appOrigin()}/reset-password?token=${encodeURIComponent(token)}`;
      await sendPasswordResetEmail({
        to: user.email,
        resetUrl,
        expiresMinutes: RESET_TTL_MINUTES,
      });
    }

    await padResponse(startedAt);
    return REQUEST_RESPONSE;
  } catch (error) {
    await padResponse(startedAt);
    throw error;
  }
}

function validatePassword(password: string) {
  if (password.length < 12) {
    throw new Error("password_too_short");
  }
  if (password.length > 128) {
    throw new Error("password_too_long");
  }
}

export async function resetPassword(input: ResetPasswordInput) {
  validatePassword(input.newPassword);

  const tokenHash = hashToken(input.token);
  const now = new Date();
  const passwordHash = await bcrypt.hash(input.newPassword, 12);

  return prisma.$transaction(async (tx) => {
    const resetToken = await tx.passwordResetToken.findFirst({
      where: {
        tokenHash,
        expiresAt: { gt: now },
        usedAt: null,
      },
      select: { id: true, userId: true },
    });

    if (!resetToken) {
      throw new Error("invalid_or_expired_token");
    }

    const claimed = await tx.passwordResetToken.updateMany({
      where: { id: resetToken.id, usedAt: null },
      data: { usedAt: now },
    });

    if (claimed.count !== 1) {
      throw new Error("invalid_or_expired_token");
    }

    await tx.user.update({
      where: { id: resetToken.userId },
      data: {
        passwordHash,
        passwordChangedAt: now,
      },
    });

    await tx.session.updateMany({
      where: { userId: resetToken.userId, revokedAt: null },
      data: { revokedAt: now },
    });

    await tx.auditLog.create({
      data: {
        userId: resetToken.userId,
        event: "password_reset_completed",
        ip: input.ip,
        userAgent: input.userAgent?.slice(0, 300),
        metadata: { sessionsRevoked: true },
      },
    });

    return { message: "Password updated. Please sign in again." };
  });
}

本番のrate limitはRedisやUpstashのような共有ストアに置き換えてください。上の Map はローカル開発や単一プロセスの検証には動きますが、複数インスタンスでは共有されません。ここをClaude Codeに任せるなら「既存のRedisクライアントを使う」「失敗時は安全側に倒す」「メール送信前に制限する」と明示します。

Next.js API routeを作る

リクエストAPIは、メールアドレスが存在しない場合でも同じJSONを返します。入力が壊れていても、アカウント存在確認に使える差を返さないようにしています。

// src/app/api/auth/password-reset/request/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { requestPasswordReset } from "@/lib/password-reset";

const bodySchema = z.object({
  email: z.string().email().max(320),
});

const genericResponse = {
  message: "If an account exists for that email, a password reset link has been sent.",
};

function clientIp(request: Request) {
  return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
}

export async function POST(request: Request) {
  let body: unknown;

  try {
    body = await request.json();
  } catch {
    return NextResponse.json(genericResponse, { status: 202 });
  }

  const parsed = bodySchema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json(genericResponse, { status: 202 });
  }

  await requestPasswordReset({
    email: parsed.data.email,
    ip: clientIp(request),
    userAgent: request.headers.get("user-agent") ?? undefined,
  });

  return NextResponse.json(genericResponse, { status: 202 });
}

確定APIは、無効なトークンや期限切れトークンには同じエラーを返します。ここでは「このリンクは使えない」という情報だけで十分です。

// src/app/api/auth/password-reset/confirm/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { resetPassword } from "@/lib/password-reset";

const bodySchema = z
  .object({
    token: z.string().min(32).max(256),
    password: z.string().min(12).max(128),
    confirmPassword: z.string().min(12).max(128),
  })
  .refine((value) => value.password === value.confirmPassword, {
    message: "password_mismatch",
    path: ["confirmPassword"],
  });

function clientIp(request: Request) {
  return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
}

export async function POST(request: Request) {
  const parsed = bodySchema.safeParse(await request.json().catch(() => null));

  if (!parsed.success) {
    return NextResponse.json(
      { message: "The reset link is invalid or the password policy was not met." },
      { status: 400 },
    );
  }

  try {
    const result = await resetPassword({
      token: parsed.data.token,
      newPassword: parsed.data.password,
      ip: clientIp(request),
      userAgent: request.headers.get("user-agent") ?? undefined,
    });

    return NextResponse.json(result, { status: 200 });
  } catch {
    return NextResponse.json(
      { message: "The reset link is invalid or expired." },
      { status: 400 },
    );
  }
}

レビューで見るべき点は、request404email not found を返していないこと、confirm で使用済みトークンを再利用できないこと、成功時にセッションを無効化していることです。

リセットページとヘッダーを設定する

URLトークン方式では、トークンがブラウザのアドレスバーに入ります。そのため、リセットページに外部解析タグや広告タグを置く場合は特に注意が必要です。外部サイトへ遷移したときにRefererヘッダーでURLごと送られると、トークンが漏れます。

Next.jsなら、ページ側とヘッダー側で二重に守るのが実務では扱いやすいです。

// src/app/reset-password/page.tsx
"use client";

import { Suspense, useState } from "react";
import { useSearchParams } from "next/navigation";

function ResetPasswordForm() {
  const token = useSearchParams().get("token") ?? "";
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const [message, setMessage] = useState("");
  const [busy, setBusy] = useState(false);

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setBusy(true);
    setMessage("");

    const response = await fetch("/api/auth/password-reset/confirm", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token, password, confirmPassword }),
    });

    const data = (await response.json()) as { message: string };
    setMessage(data.message);
    setBusy(false);
  }

  return (
    <main className="mx-auto flex min-h-screen max-w-md flex-col justify-center px-6">
      <h1 className="mb-3 text-2xl font-semibold">Set a new password</h1>
      <p className="mb-6 text-sm text-gray-600">
        Use at least 12 characters. After the reset, all existing sessions will be signed out.
      </p>
      <form onSubmit={onSubmit} className="space-y-4">
        <input
          type="password"
          autoComplete="new-password"
          minLength={12}
          maxLength={128}
          required
          value={password}
          onChange={(event) => setPassword(event.target.value)}
          className="w-full rounded-md border px-3 py-2"
          placeholder="New password"
        />
        <input
          type="password"
          autoComplete="new-password"
          minLength={12}
          maxLength={128}
          required
          value={confirmPassword}
          onChange={(event) => setConfirmPassword(event.target.value)}
          className="w-full rounded-md border px-3 py-2"
          placeholder="Confirm new password"
        />
        <button
          type="submit"
          disabled={busy || !token}
          className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white disabled:opacity-60"
        >
          {busy ? "Updating..." : "Update password"}
        </button>
      </form>
      {message ? <p className="mt-4 text-sm text-gray-700">{message}</p> : null}
    </main>
  );
}

export default function ResetPasswordPage() {
  return (
    <Suspense fallback={null}>
      <ResetPasswordForm />
    </Suspense>
  );
}
// src/app/reset-password/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Reset password",
  robots: {
    index: false,
    follow: false,
  },
  referrer: "no-referrer",
};

export default function ResetPasswordLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return children;
}
// next.config.mjs
const nextConfig = {
  async headers() {
    return [
      {
        source: "/reset-password",
        headers: [
          { key: "Referrer-Policy", value: "no-referrer" },
          { key: "X-Robots-Tag", value: "noindex, nofollow" },
          { key: "Cache-Control", value: "no-store" },
        ],
      },
    ];
  },
};

export default nextConfig;

Referrer-Policy は「どのページから来たかを次のサイトへどこまで送るか」のルールです。no-referrer にすると、リセットURLのクエリにあるトークンが外部へ送られる事故を避けやすくなります。

4つの実例で設計を確認する

1つ目は、SaaSの一般ユーザー向けリセットです。ユーザーが夜にパスワードを忘れてメールから再設定します。この場合は30分の期限、1回使用、成功後の全セッション無効化が効きます。古いスマホでログインしたままのセッションも落ちるので、盗まれた端末が残っていても被害を抑えられます。

2つ目は、管理者アカウントのリセットです。管理者は権限が強いので、パスワードリセット成功後に追加の再認証やMFA確認を要求します。ただし、パスワードリセットだけでMFAを解除してはいけません。MFA端末をなくした場合は、本人確認、サポート承認、監査ログ、待機時間を持つ別フローに分けます。二要素認証の実装 とセットで設計する領域です。

3つ目は、問い合わせ対応です。「リセットメールが届かない」という連絡が来たとき、サポート担当者は監査ログで password_reset_requested の時刻、送信先、IP、User-Agent、メール配信エラーを確認します。ここでトークン本体を見られる設計にしていると、サポート権限の悪用が可能になります。ログにはトークンを残さず、配信IDとイベントだけを残すのが安全です。

4つ目は、B2Bで退職者のアカウントが残っていたケースです。攻撃者が既知の会社メールへ大量にリセットを投げると、メールボックスが荒れます。IP単位とメール単位のrate limitを先に置いておくと、問い合わせが来る前に異常を検知できます。

よくある失敗例と落とし穴

もっとも多い失敗は、存在しないメールに そのメールアドレスは登録されていません と返すことです。攻撃者はこの差分だけで顧客リストを作れます。応答時間の差も同じ問題になります。存在するユーザーだけメール送信やDB処理で遅くなると、タイミングで推測されます。

次に危ないのは、トークンを平文でDBに保存することです。DBの読み取り権限を持つ人、ログに出たSQL、バックアップ、開発用ダンプからリセットURLを組み立てられます。保存するのはハッシュだけにします。

3つ目は、期限が長すぎる、または何度でも使えるトークンです。メールは転送され、ブラウザ履歴に残り、スクリーンショットにも写ります。30分程度で期限切れにし、DBトランザクション内で一度だけ usedAt を埋める形にしてください。

4つ目は、Host ヘッダーからリセットURLを作ることです。攻撃者が細工したHostでリクエストすると、メール内リンクが攻撃者のドメインになることがあります。APP_ORIGIN のような固定のHTTPSオリジンを環境変数で持ち、環境変数管理 のレビュー対象にします。

5つ目は、パスワードリセット成功後に自動ログインすることです。便利に見えますが、セッション処理が複雑になり、既存セッションの扱いも曖昧になります。リセット後は全セッションを無効化し、通常のログイン画面へ戻すほうがレビューしやすいです。

6つ目は、MFA再設定を同じリンクで許すことです。パスワードを取り戻せる人が、同時にMFAも外せるなら、MFAの防御力は大きく落ちます。MFA再設定は、別チャネル、本人確認、管理者承認、通知、監査ログを持つ別フローにします。

Claude Code出力のレビュー観点

Claude Codeで生成した差分は、次のチェックリストでレビューします。

  • APIのレスポンスが、存在するメールと存在しないメールで同じか。
  • 早期returnにより、存在しないメールだけ明らかに速くなっていないか。
  • tokenpasswordresetUrl をログに出していないか。
  • DBに保存しているのがトークン本体ではなく tokenHash か。
  • expiresAtusedAt の両方で期限切れ・一回使用を見ているか。
  • updateMany などで同時送信時の二重使用を防いでいるか。
  • 成功後に Session をrevokeし、自動ログインしていないか。
  • Referrer-PolicyX-Robots-TagCache-Control がリセットページに付くか。
  • MFAを解除・再登録する処理が混ざっていないか。
  • 監査ログに requestedcompleted の両方が残るか。

権限面では、Claude Codeに本番メール送信キーや本番DBの接続情報を渡さないことも重要です。ローカルの .env.example とテストDBで作業させ、本番秘密情報はCI/CDやホスティング側のsecret storeに置きます。

動作確認の最小セット

最後に、最低限の確認をコマンド化します。ここまでClaude Codeに作らせると、レビューがかなり楽になります。

npm install @prisma/client bcryptjs zod
npx prisma migrate dev --name add_password_reset
npm run lint
npm test

API単体の確認は次のようにできます。存在するメールでも存在しないメールでも、同じステータスと同じメッセージになることを確認します。

curl -i -X POST http://localhost:3000/api/auth/password-reset/request \
  -H "Content-Type: application/json" \
  -d '{"email":"nobody@example.com"}'

curl -i -X POST http://localhost:3000/api/auth/password-reset/request \
  -H "Content-Type: application/json" \
  -d '{"email":"real-user@example.com"}'

トークン再利用の確認では、同じトークンで2回 confirm を叩き、1回目だけ成功することを見ます。さらに、リセット後に古いセッションIDで保護APIへアクセスし、拒否されることも確認します。

curl -i -X POST http://localhost:3000/api/auth/password-reset/confirm \
  -H "Content-Type: application/json" \
  -d '{"token":"PASTE_TOKEN","password":"new-long-password-123","confirmPassword":"new-long-password-123"}'

まとめ

パスワードリセットは、フォーム、メール、DB、セッション、監査ログ、MFA方針が絡む小さな認証システムです。Claude Codeに任せるなら、単にUIを作らせるのではなく、ユーザー存在を漏らさないレスポンス、ランダムトークン、ハッシュ保存、短い有効期限、一回使用、rate limit、メールテンプレート、Referrer-Policynoindex、セッション無効化、監査ログまでを受け入れ条件にしてください。

ClaudeCodeLabでは、Claude Codeを使った認証機能の設計レビュー、研修、実装支援も扱っています。既存のログイン・MFA・メール配信・セッション管理を見せてもらえれば、どこまでClaude Codeに任せ、どこを人間がレビューすべきかを現実的な粒度に落とし込めます。

この記事で紹介した内容を実際に試すときの確認ポイントは、同じメールリセットAPIを存在するアカウントと存在しないアカウントで叩き、レスポンス本文・HTTPステータス・応答時間が近いこと、DBに平文トークンが残っていないこと、期限切れと使用済みトークンが拒否されること、リセット後に既存セッションが無効化されることです。Masaが検証するときも、まずこの4点を手で確認してから、自動テストに落とし込んでいます。

#Claude Code #パスワードリセット #認証 #セキュリティ #メール
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。