👏

Drizzle+remix-auth+Cloudflare Workers+Cloudflare D1で簡易認証機能を作成する

に公開

はじめに

RemixとCloudflare D1を使用して、シンプルな管理画面認証を実装する方法を解説します。特に認証周りの実装に焦点を当てます。前提としてレコードの登録はDrizzle Studioなどを使って、IDとパスワードをハッシュ化したものを入れておきます。

実装の概要

  • Remix Authを使用した認証
  • Cloudflare D1をデータベースとして使用
  • DrizzleORMでのデータベース操作

データベーススキーマ

app/db/schema.ts
export const adminsSchema = sqliteTable("admins", {
  id: text("id").primaryKey(),
  email: text("email").notNull().unique(),
  password: text("password").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
});

認証ストラテジーの実装

app/strategies/admin-form.ts
export function getAdminFormStrategy(env: Env) {
  const db = drizzle(env.DB, { schema });

  return new FormStrategy<Admin>(async ({ form }) => {
    const email = form.get("email");
    const password = form.get("password");

    if (!email || !password) {
      throw new Error("Invalid form submission");
    }

    const admin = await db.query.adminsSchema.findFirst({
      where: eq(adminsSchema.email, email),
    });

    if (!admin || !(await bcrypt.compare(password, admin.password))) {
      throw new Error("Invalid credentials");
    }

    return admin;
  });
}

ログインページの実装

app/routes/admin.login.tsx
export async function action({ request, context }: ActionFunctionArgs) {
  const { env } = context.cloudflare;
  const authenticator = new Authenticator<Admin>();
  authenticator.use(getAdminFormStrategy(env), "admin");

  try {
    const admin = await authenticator.authenticate("admin", request);
    const session = await sessionStorage.getSession(request.headers.get("cookie"));
    session.set("admin", admin);

    return redirect("/admin", {
      headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
    });
  } catch (error) {
    if (error instanceof Response) throw error;
    return { error: "ログインに失敗しました" };
  }
}

重要なポイント:Authenticatorのインスタンス生成

従来の実装方法

通常、Remix Authの実装では、以下のようにauth.server.tsでAuthenticatorのインスタンスを作成します:

auth.server.ts
// 従来の実装(この方法は今回使えない)
export const authenticator = new Authenticator<Admin>(sessionStorage);
authenticator.use(strategy, "admin");

Cloudflare D1対応の実装

Cloudflare D1を使用する場合、envオブジェクトはリクエストのたびにCloudflareから提供されます。そのため、以下の変更が必要です:

  1. Authenticatorのインスタンスをリクエストごとに生成
  2. ストラテジーにenvを渡して初期化
  3. action関数内でこれらの処理を実行
admin.login.tsx
export async function action({ request, context }: ActionFunctionArgs) {
  const { env } = context.cloudflare;
  // リクエストごとにAuthenticatorを初期化
  const authenticator = new Authenticator<Admin>();
  // envをストラテジーに渡す
  authenticator.use(getAdminFormStrategy(env), "admin");
  // ...
}

この実装方法により:

  • Cloudflare D1への接続をリクエストごとに適切に管理
  • 環境変数やコンテキストを正しくストラテジーに渡すことが可能
  • サーバーレス環境での適切な認証処理の実現

まとめ

Cloudflareの環境で認証を実装する際は、従来のシングルトンパターンではなく、リクエストごとの初期化が必要になります。これにより、サーバーレス環境での適切なリソース管理と認証処理が実現できます。
これが良さそうな方法だと思い実装しましたが、より良い方法があればアドバイスいただければと思います。

Discussion