ProductReadyProductReady

認証

Better Authによるユーザー認証 - サインアップ、ログイン、OAuth、セッション管理

認証

ProductReadyは、モダンで安全な認証のためにBetter Authを使用しています。このガイドでは、基本的なメール/パスワードからOAuth統合まですべてをカバーします。

なぜBetter Auth? モダン、型安全、フレームワークに依存しない、活発にメンテナンスされています。NextAuth/Auth.jsよりもはるかにシンプルです。


クイック概要

サポートされる方法

  • ✅ メール + パスワード
  • ✅ OAuth(GitHub、Google)
  • ✅ マジックリンク(メールのみのログイン) - 設定可能
  • ✅ セッション管理
  • ✅ メール確認 - オプション

仕組み

  1. ユーザーがサインアップ/ログイン
  2. Better Authがセッションを作成
  3. セッションがデータベース + Cookieに保存
  4. tRPCが保護されたルートのセッションをチェック

サインアップとログイン

メール + パスワードフロー

ユーザーはメールとパスワードでアカウントを作成できます。

サインアップページ: /sign-up(またはカスタムの場所)

コンポーネント例

'use client';

import { authClient } from '~/lib/auth.client';
import { useState } from 'react';

export function SignUpForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    
    try {
      await authClient.signUp.email({
        email,
        password,
        name,
      });
      
      // ダッシュボードにリダイレクト
      window.location.href = '/dashboard';
    } catch (error) {
      console.error('サインアップ失敗:', error);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="名前"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="email"
        placeholder="メール"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        placeholder="パスワード"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">サインアップ</button>
    </form>
  );
}

ログインも同様です - authClient.signIn.email()を使用:

await authClient.signIn.email({
  email,
  password,
});

OAuth認証

GitHubでログイン

設定

  1. GitHub OAuth Appを作成

    • GitHub Developer Settingsにアクセス
    • 「New OAuth App」をクリック
    • コールバックURL:http://localhost:3000/api/auth/callback/github
  2. 環境変数を設定

BETTER_AUTH_GITHUB_CLIENT_ID=your_github_client_id
BETTER_AUTH_GITHUB_CLIENT_SECRET=your_github_client_secret
  1. コンポーネントで使用
import { authClient } from '~/lib/auth.client';

export function GitHubSignIn() {
  async function handleGitHubSignIn() {
    await authClient.signIn.social({
      provider: 'github',
      callbackURL: '/dashboard',
    });
  }

  return (
    <button onClick={handleGitHubSignIn}>
      GitHubでサインイン
    </button>
  );
}

Googleでログイン

同様のプロセス:

  1. Google Cloud ConsoleでOAuthクライアントを作成
  2. 環境変数を設定:
BETTER_AUTH_GOOGLE_CLIENT_ID=your_google_client_id
BETTER_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret
  1. コンポーネントで使用:
await authClient.signIn.social({
  provider: 'google',
  callbackURL: '/dashboard',
});

セッション管理

サーバーサイドでセッションを確認

tRPCルーター内

import { protectedProcedure } from '~/server/trpc';

export const userRouter = router({
  getProfile: protectedProcedure
    .query(async ({ ctx }) => {
      // ctx.sessionにユーザー情報が含まれます
      const userId = ctx.session.user.id;
      
      return await ctx.db.query.users.findFirst({
        where: eq(users.id, userId),
      });
    }),
});

クライアントサイドでセッションを確認

Reactコンポーネント内

'use client';

import { authClient } from '~/lib/auth.client';
import { useEffect, useState } from 'react';

export function ProfilePage() {
  const [session, setSession] = useState(null);

  useEffect(() => {
    authClient.getSession().then(setSession);
  }, []);

  if (!session) {
    return <div>読み込み中...</div>;
  }

  return (
    <div>
      <h1>ようこそ、{session.user.name}さん</h1>
      <p>メール: {session.user.email}</p>
    </div>
  );
}

ログアウト

import { authClient } from '~/lib/auth.client';

export function LogoutButton() {
  async function handleLogout() {
    await authClient.signOut();
    window.location.href = '/';
  }

  return (
    <button onClick={handleLogout}>
      ログアウト
    </button>
  );
}

ルートの保護

ページレベルの保護

Server Component

import { auth } from '~/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await auth();
  
  if (!session) {
    redirect('/sign-in');
  }

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ、{session.user.name}さん</p>
    </div>
  );
}

API保護

tRPCではprotectedProcedureを使用:

import { protectedProcedure, router } from '~/server/trpc';

export const tasksRouter = router({
  list: protectedProcedure
    .query(async ({ ctx }) => {
      // セッションは既に検証済み
      const userId = ctx.session.user.id;
      
      return await ctx.db.query.tasks.findMany({
        where: eq(tasks.userId, userId),
      });
    }),
});

パスワードリセット

リセットをリクエスト

await authClient.forgetPassword({
  email: 'user@example.com',
  redirectTo: '/reset-password',
});

パスワードをリセット

await authClient.resetPassword({
  token: 'reset_token_from_email',
  password: 'new_password',
});

メール確認

確認メールを送信

await authClient.sendVerificationEmail({
  email: 'user@example.com',
  callbackURL: '/verify',
});

メールを確認

await authClient.verifyEmail({
  token: 'verification_token_from_email',
});

ベストプラクティス

セキュリティ

  • 強力なパスワードを強制:最低8文字、大文字、数字
  • レート制限:ログイン試行を制限
  • HTTPS:本番環境では常にHTTPSを使用
  • 環境変数:シークレットをコードにハードコードしない

ユーザーエクスペリエンス

  • 明確なエラーメッセージ:ユーザーに何が問題かを伝える
  • ローディング状態:非同期操作中にローダーを表示
  • リダイレクト:ログイン後、ユーザーを適切な場所に誘導
  • 「ログイン状態を保持」:セッションの有効期限を長くするオプション

トラブルシューティング

セッションが持続しない

問題:ログイン後、ページリフレッシュでログアウトされる。

解決策

  1. Cookieが正しく設定されていることを確認
  2. BETTER_AUTH_SECRETが設定されていることを確認
  3. 開発環境ではlocalhostを使用(127.0.0.1ではなく)

OAuth リダイレクトが機能しない

問題:OAuth後に404エラー。

解決策

  1. OAuthプロバイダーのコールバックURLが正しいことを確認
  2. .envファイルに正しいクライアントIDとシークレットがあることを確認
  3. サーバーを再起動

次のステップ

認証を設定したので:

  1. データベースガイド - ユーザーデータを保存
  2. tRPCガイド - 保護されたAPIを構築
  3. 組織管理 - チームとスペースを追加

参考資料

ProductReadyで認証を楽しんでください! 🔐

On this page