🌐

NextAuth.js + LINE Login による認証システムの実装

に公開

はじめに

この記事では、Next.jsアプリケーションにLINE Loginを実装する方法を、初学者でも理解できるように詳しく説明します。

認証とは?

認証(Authentication)とは、「この人は本当に本人なのか?」を確認する仕組みです。

  • ログイン: 本人確認のための手続き
  • セッション: ログイン状態を維持する仕組み
  • ログアウト: セッションを終了する手続き

NextAuth.jsとは?

NextAuth.jsは、Next.jsアプリケーションで認証機能を簡単に実装できるライブラリです。

従来の方法(大変):

// 手動でOAuth認証を実装する場合
const authUrl = `https://access.line.me/oauth2/v2.1/authorize?` +
  `response_type=code&` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${REDIRECT_URI}&` +
  `state=${randomState}&` +
  `scope=profile%20openid`;

// コールバック処理も手動で実装
app.get('/callback', async (req, res) => {
  // 複雑な処理...
});

NextAuth.jsを使う場合(簡単):

// 設定だけで完結
LineProvider({
  clientId: process.env.LINE_CLIENT_ID!,
  clientSecret: process.env.LINE_CLIENT_SECRET!,
})

実装手順

  1. 必要なパッケージのインストール(パッケージマネージャはなんでもいいです)
    補足:パッケージマネージャとは?
    pnpm install next-auth
    
  2. NextAuth.jsの設定ファイル作成
    app/api/auth/[...nextauth]/route.tsを作成:
import NextAuth from "next-auth";
import LineProvider from "next-auth/providers/line";
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

const handler = NextAuth({
  // デバッグモードを有効化(開発時のみ)
  debug: true,
  
  // プロバイダー設定(LINE Login)
  providers: [
    LineProvider({
      clientId: process.env.LINE_CLIENT_ID!,
      clientSecret: process.env.LINE_CLIENT_SECRET!,
      authorization: {
        params: {
          scope: "profile openid",
        },
      },
    }),
  ],
  
  // コールバック関数(認証の各段階で実行される)
  callbacks: {
    // ログイン時の処理
    async signIn({ user, account, profile }) {
      console.log('ログイン処理開始:', { user, account, profile });
      
      if (account?.provider === 'line' && profile) {
        try {
          // データベースにユーザー情報を保存
          await sql`
            INSERT INTO users (id, name, created_at)
            VALUES (${profile.sub}, ${profile.name}, NOW())
            ON CONFLICT (id) DO NOTHING
            RETURNING id
          `;
          console.log('ユーザー情報をデータベースに保存しました');
        } catch (error) {
          console.error('データベース保存エラー:', error);
        }
      }
      return true; // ログインを許可
    },
    
    // JWTトークンに情報を追加
    async jwt({ token, account, profile }) {
      if (account && profile) {
        token.lineUserId = profile.sub;    // LINEのユーザーID
        token.lineUserName = profile.name; // LINEのユーザー名
      }
      return token;
    },
    
    // セッションに情報を追加
    async session({ session, token }) {
      if (token.lineUserId) {
        session.user.lineUserId = token.lineUserId as string;
        session.user.lineUserName = token.lineUserName as string;
      }
      return session;
    },
  },
});

export { handler as GET, handler as POST };
  1. 型定義の拡張
    types/next-auth.d.tsを作成:
import NextAuth from "next-auth"

declare module "next-auth" {
  interface Session {
    user: {
      lineUserId: string
      lineUserName: string
    }
  }

  interface User {
    lineUserId: string
    lineUserName: string
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    lineUserId: string
    lineUserName: string
  }
}
  1. プロバイダーの設定
    app/providers.tsxを作成:
'use client';

import { SessionProvider } from 'next-auth/react';

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  );
}
  1. レイアウトにプロバイダーを追加
    app/layout.tsxを更新:
import Providers from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}
  1. 認証フックの作成
    hooks/useAuth.tsを作成:
'use client';
import { useEffect } from 'react';
import { useSession, signIn, signOut } from 'next-auth/react';
import { useAuthStore } from '@/stores/authStore';

export const useAuth = () => {
  // NextAuth.jsのセッション情報を取得
  const { data: session, status } = useSession();
  const { 
    isLineBrowser, 
    user, 
    setLineBrowser, 
    setUser, 
    logout 
  } = useAuthStore();

  // LINE内ブラウザかどうかを判定
  useEffect(() => {
    const userAgent = navigator.userAgent.toLowerCase();
    const isLine = userAgent.includes('line');
    setLineBrowser(isLine);
  }, [setLineBrowser]);

  // NextAuth.jsのセッション状態をZustandストアに同期
  useEffect(() => {
    if (status === 'loading') return;

    if (session?.user?.lineUserId && session?.user?.lineUserName) {
      setUser({
        lineUserId: session.user.lineUserId,
        lineUserName: session.user.lineUserName,
      });
    } else {
      setUser(null);
    }
  }, [session, status, setUser]);

  // LINE内ブラウザでの自動ログイン
  useEffect(() => {
    if (status === 'unauthenticated' && isLineBrowser) {
      signIn('line');
    }
  }, [isLineBrowser, status]);

  const handleSignIn = () => signIn('line');
  const handleSignOut = () => {
    signOut();
    logout();
  };

  return {
    isLineBrowser,
    user,
    isAuthenticated: !!user,
    isLoading: status === 'loading',
    signIn: handleSignIn,
    signOut: handleSignOut,
  };
};
  1. グローバル状態管理(Zustand)
    stores/authStore.tsを作成:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  lineUserId: string;
  lineUserName: string;
}

interface AuthState {
  isLineBrowser: boolean;
  user: User | null;
  isAuthenticated: boolean;
  setLineBrowser: (isLine: boolean) => void;
  setUser: (user: User | null) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      isLineBrowser: false,
      user: null,
      isAuthenticated: false,
      setLineBrowser: (isLine) => set({ isLineBrowser: isLine }),
      setUser: (user) => set({ 
        user, 
        isAuthenticated: !!user 
      }),
      logout: () => set({ 
        user: null, 
        isAuthenticated: false 
      }),
    }),
    {
      name: 'auth-storage', // ローカルストレージのキー名
    }
  )
);
  1. コンポーネントでの使用例
    app/page.tsxでの使用:
'use client';

import { Button } from "@/components/ui/button";
import { useAuth } from "@/hooks/useAuth";

export default function Home() {
  const { user, isLoading, signIn, signOut, isLineBrowser } = useAuth();

  // ローディング中の表示
  if (isLoading) {
    return (
      <div className="text-center">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
        <p className="text-gray-600">
          {isLineBrowser ? 'LINEでログイン中...' : '認証情報を確認中...'}
        </p>
      </div>
    );
  }

  // 未ログイン時の表示
  if (!user) {
    return (
      <div className="text-center space-y-4">
        <h2 className="text-xl font-bold">Planlyへようこそ</h2>
        <p className="text-gray-600">
          LINEアカウントでログインして、イベントの管理を始めましょう
        </p>
        <Button onClick={signIn} className="w-full">
          LINEでログイン
        </Button>
      </div>
    );
  }

  // ログイン済みの表示
  return (
    <div className="text-center space-y-4">
      <h2 className="text-xl font-bold">ようこそ、{user.lineUserName}さん</h2>
      <Button onClick={signOut}>
        ログアウト
      </Button>
    </div>
  );
}
  1. 最後に
    環境変数の設定
    .env.localファイルを作成:
# LINE Login設定
LINE_CLIENT_ID=your_line_client_id
LINE_CLIENT_SECRET=your_line_client_secret

# NextAuth.js設定
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secret_key

# データベース設定
DATABASE_URL=your_database_url

全体のデータの流れ

  1. ユーザーがログインボタンをクリック
  2. NextAuth.jsがLINEの認証画面にリダイレクト
  3. LINEで認証完了後、コールバックURLに戻る
  4. NextAuth.jsが認証情報を処理
  5. データベースにユーザー情報を保存
  6. セッションを作成してクッキーに保存
  7. アプリケーションでログイン状態を利用

Discussion