nino
  • ドキュメント
  • レジストリ
  • メンバーシップ

Command Palette

Search for a command to run...

スポンサー
メニュー

Better Auth

 

10分 9秒

Better Auth とは

Better Auth は、認証を簡単に設定できる次世代の認証ライブラリです。

  1. 認証の設定を定義
  2. 認証に必要な ORMスキーマを生成(マイグレーション)
  3. 動作

となります。スキーマも一般的な内容であるためロックインを気にせず導入できます。

ライブデモ

インストール

Loading...
Loading...

Base URL 判定

(推奨)Better Auth はアプリが動作するURLを知る必要がありますが、Vercelの場合はプレビュー環境ごとにURLが生成され、この判定が複雑になるため既存のヘルパーを導入します。

Loading...
マニュアルで作成する場合
Loading...

インスタンスの作成

Drizzle を使う想定です。サーバー用のインスタンスとクライアント用のインスタンスを用意します。サーバー用のインスタンスは Better Auth の設定も兼ねており、ORMスキーマの生成に影響します。

今回は匿名ログインをベースに進めます。

ヒント

Better Auth がはじめての場合や開発初期の場合、匿名ログインを有効にすることをお勧めします。匿名ログインを使うとメールアドレスやSNSアカウントを使わずにログインできます。

サーバー用のインスタンス

サーバーサイドから認証機能にアクセスする場合は、サーバー用のインスタンスを作成します。

Loading...

クライアント用のインスタンス

"use client" を使うクライアントコンポーネントから認証機能にアクセスする場合は、クライアント用のインスタンスを作成します。

Loading...

ルートハンドラー

Better Auth を動作させるために、ルートハンドラーを作成します。

Loading...

Proxy の設定

認証状態に応じてリダイレクトする場合、Proxy を設定します。たとえばログインしていないユーザーをログインページにリダイレクトする場合、以下のように設定します。

注意

ここで行っているのは画面やデータの保護ではなく、あくまで「ログインしてないユーザーをログイン画面に飛ばす」というシンプルなリダイレクト処理です。データの保護は別途データ取得関数で行なってください。

くわしく

ここでは getSessionCookie を使って Cookie の内容でログイン状態を判断していますが、Cookie は誰でも書き換えることができます。データベースにログイン状態を問い合わせないので高速ですが、信頼性は低くなります。

ただし基本的には画面に必要なデータを取得するタイミングでサーバーサイドの検証を行うため問題ありません。たとえCookieを偽装して画面にアクセスしても、肝心のデータ取得が失敗するので情報漏洩にはなりません。

Loading...

データベースの更新

認証機能を動作させるために、データベースにテーブルを追加する必要があります。

スキーマの生成

ORM 用のスキーマを生成します。--output で出力先を指定します。実際のスキーマの場所を指定してください。

Loading...

lib/auth.ts に応じてデータベースも更新する必要があるので、その都度上記コマンドを実行してスキーマを再生成します。この観点から、生成された auth.ts を直接編集するのはお勧めしません。

今後のためにスクリプトを用意しておくと便利です。

Loading...

マイグレーション

スキーマができたら ORM の設定に生成されたスキーマを加え、マイグレーションを実行します。

Loading...
Loading...

データの保護

画面のリダイレクトは proxy.ts で Cookie を参照して楽観的に行っていますが、データの保護はデータ通信関数で行います。

保護用のヘルパー関数

getSession と currentSession を作成します。

  • getSession - セッションの有無を確認する場合に使用
  • currentSession - セッションの存在を担保する場合に使用
Loading...

redirect を使うとその時点で処理が強制終了するため、currentSession を挟むことでログインしていないユーザーの処理を中断できます。

データベースとの通信関数では基本的に currentSession を使ってログイン状態を担保しつつログイン中のユーザーを特定し、そのユーザーのIDに基づいてデータベースを操作します。

安全なデータの取得

Loading...

安全なデータの更新

Loading...

個人の特定はサーバーサイドで行う

たとえば自分のプロフィールを更新する場合、自分が誰であるかは必ずサーバーサイドで特定します。

Loading...

ログアウト

サーバーアクションと refresh を使ってログアウトおよび即時反映ができます。

ボタンコンポーネント

Loading...

ログイン状態に応じたUIの出しわけ

ログイン不要でみれて良い画面の場合、getSession を使ってログイン状態を確認します。 currentSession を使うとログインしていない場合リダイレクトされてしまいます。

Loading...
Discord で質問する
何か気になったこと、分からないことがあれば気軽に質問してください!
DiscordDiscordで質問する
nino

Developer

XXGitHubGitHubYouTubeYouTubeZennZennDiscordDiscord

    リンク

  • ドキュメント
  • レジストリ
  • アーキテクチャ

    ツール

  • フィード
  • ステータス

    ポリシー

  • 利用規約
  • プライバシーポリシー
  • 特定商取引法に基づく表示
ドキュメント
はじめに
  • Changelog
ガイド
  • Webアプリの環境構築
  • ニュース収集&AI要約
  • Turso のテーブル移行
  • 日時の管理
  • 中規模のリストをブラウザにキャッシュし、クライアントでフィルタする
  • Search Params Dialog
  • コードの整理
  • AGENTS.md
  • Better Auth
  • プロダクト開発ポリシー
  • 推奨ツール
  • Proxy.ts
  • 多言語対応
  • SWR
  • Cache Component
  • Next.js の課題、不具合
  • アプリ開発フロー
  • プロンプトガイド
  • CSS Tips
  • Font Family
  • セルフブランディング
  • セルフオンボーディング
  • オフィスツール
  • Email
  • Breadcrumb(パンくず)
  • 並べ替え
pnpm add better-auth

# SECRET_KEYを生成
pnpx @better-auth/cli@latest secret
BETTER_AUTH_SECRET=生成した内容をセット
pnpx shadcn@latest add https://dninomiya.com/r/base-url.json
export const baseUrl = (options?: { useCommitURL?: boolean }) => {
  const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
  const url = isProd
    ? process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL
    : options?.useCommitURL
      ? process.env.NEXT_PUBLIC_VERCEL_URL
      : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL;

  return url
    ? `https://${url}`
    : `http://localhost:${process.env.PORT || 3000}`;
};
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { anonymous } from "better-auth/plugins"
import { nextCookies } from "better-auth/next-js";
import { db } from "@/db"; // dbクライアントの場所
import { baseUrl } from "@/lib/base-url"; // baseURLの場所

export const auth = betterAuth({
  baseURL: baseUrl(),
  database: drizzleAdapter(db, {
      provider: "sqlite",
      usePlural: true, // スキーマを複数形に
  }),
  socialProviders: {
    // ソーシャルログインの設定
    // github: {
    //   clientId: process.env.GITHUB_CLIENT_ID as string,
    //   clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
    // },
  },
  plugins: [
    nextCookies(),
    anonymous(), // 匿名ログイン
  ]
});
import { createAuthClient } from "better-auth/react";
import {
  anonymousClient,
  inferAdditionalFields,
} from "better-auth/client/plugins";
import { auth } from "./auth";

export const authClient = createAuthClient({
  baseURL: baseUrl(),
  plugins: [
    inferAdditionalFields<typeof auth>(),
    anonymousClient(),
  ],
});
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth.handler);
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";

// 公開ルート
const publicRoutes = [
  "/",
  "/login",
];

export async function proxy(request: NextRequest) {
	const sessionCookie = getSessionCookie(request);
  const isPrivateRoute = !publicRoutes.includes(request.nextUrl.pathname);

  // ログインしてないユーザーかつ公開ルートでない場合はログイン画面にリダイレクト
	if (!sessionCookie && isPrivateRoute) {
		return NextResponse.redirect(new URL("/login", request.url));
	}

	return NextResponse.next();
}

export const config = {
  matcher:
    "/((?!api|monitoring|trpc|_next|_vercel|rss/|llms\\.txt|r/|.*\\..*).*)",
};
pnpx @better-auth/cli@latest generate --output ../db/schemas/auth.ts --yes
{
  "scripts": {
    "generate:auth": "pnpx @better-auth/cli@latest generate --output ../db/schemas/auth.ts --yes"
  }
}
import { drizzle } from "drizzle-orm/libsql/web";
import { auth } from "@/db/schemas/auth"; // 生成されたスキーマの場所

export const db = drizzle({
  connection: {
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  },
  schema: {
    ...auth, 
  },
});
pnpm generate && pnpm migrate
import "server-only";

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

// セッションを取得
export const getSession = async () => {
  return await auth.api.getSession({
    headers: await headers(),
  });
};

// セッションを取得(ない場合リダイレクト)
export const currentSession = async () => {
  const session = await getSession();

  if (!session) {
    redirect("/login");
  }

  return session;
};
import "server-only";

import { currentSession } from "@/lib/session";

export const getMyProfile = async () => {
  const session = await currentSession(); // ログインが担保され、ユーザーも特定される
  const userId = session.user.id;
  const profile = await db.query.profiles.findFirst({
    where: eq(profiles.userId, userId), // ログイン中のユーザーのプロフィールを取得
  });
}
"use server";

import { currentSession } from "@/lib/session";

// 自分のプロフィールを更新
export const updateMyProfile = async () => {
  const session = await currentSession(); // ログインが担保され、ユーザーも特定される
  const userId = session.user.id;
  const profile = await db.query.profiles.findFirst({
    where: eq(profiles.userId, userId), // ログイン中のユーザー自身であることを担保
  });
}

// 記事を追加
export const addArticle = async () => {
  const session = await currentSession(); // ログインが担保され、ユーザーも特定される
  const userId = session.user.id;
  const article = await db.insert(articles).values({
    authorId: userId, // 記事の投稿者
    title: "新しい記事",
    content: "これは新しい記事です。",
  });
}
"use server";

import { currentSession } from "@/lib/session";

// ❌ 悪い例: クライアントから渡されるIDが本人のものとは限らない
export const updateMyProfile = async (id: string) => {
  const profile = await db.update(profiles).set({
    name: "新しい名前",
  }).where(eq(profiles.userId, id)); // 他人のプロフィールを更新できてしまう
}

// ✅ 良い例: サーバーサイドでログイン中のユーザーを特定してからプロフィールを取得
export const updateMyProfile = async () => {
  const session = await currentSession(); // ログインが担保され、ユーザーも特定される
  const userId = session.user.id;
  const profile = await db.update(profiles).set({
    name: "新しい名前",
  }).where(eq(profiles.userId, userId)); // ログイン中のユーザー自身であることを担保
}
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { refresh } from "next/cache";

export function LogoutButton() {
  return (
    <form action={async () => {
      "use server";
      await auth.api.signOut({
        headers: await headers(),
      });
      refresh();
    }}>
      <Button>
        ログアウト
      </Button>
    </form>
  );
}
import { getSession } from "@/lib/session";
import { LoginButton } from "@/components/login-button";

export async function Page() {
  const session = await getSession();

  if (!session) {
    return (
      <div>
        <p>ログインしてください</p>
        <LoginButton />
      </div>
    );
  }

  return (
    <div>
      <p>ログインしています</p>
      <p>ようこそ、{session.user.name}さん</p>
      <LogoutButton />
    </div>
  );
}