初めてでもできるNext.js(App Router) × Supabase【認証編】

Featured image of the post

はじめに

Next.js(App Router)でSupabaseの認証を使う方法を画像付きで丁寧にまとめる😊

初めて触るときにつまづいたポイントも解説する✨

対象読者
  • App Router × Supabase の組み合わせで使いたい!
  • 素早く認証機能を作りたい!

この記事で解説すること
  • 環境構築
    • Next.js(App Router)のプロジェクトを新規作成する。
    • そこにSupabaseを導入する。

  • App RouterでSupabaseの認証を使う方法
    • 基本的なサインアップ、ログインのやり方を解説する。

  • 完成イメージ

    「メールアドレス」と「パスワード」でサインアップ、ログインする最小限のページ。

    Image in a image block

💡
一から丁寧に解説するので手順どおり真似すればOK!

つまづいたポイント

Supabaseを使うためのパッケージが2種類あり使い分けが分からなかった💦

💡
調べていると色々なやり方が出てきてどれを参考にすればいいか困った…

結論

✅古いパッケージから新しいパッケージに移行している途中らしいので、今回は新しいパッケージを使う!

  • 古いパッケージ「auth-helpers」

    このパッケージは使わない!(検索するとこの情報が多くヒットする)

  • 新しいパッケージ「@supabase/ssr」

    このパッケージを使う!(新しい方に移行中らしいのでこっちを使う)

環境構築

使用する技術

事前にインストールしておくもの

  • Node.js
  • npm

今からインストールするもの

  • Next.js
  • Supabase

Next.jsのプロジェクトを新規作成

supabase-sample-appという名前のプロジェクトを作成する。

npx create-next-app supabase-sample-app --ts --no-tailwind --eslint --app --src-dir --import-alias '@/*'

作成するとこんな感じになる。

Image in a image block

プロジェクトに移動

supabase-sample-appに移動する。

cd supabase-sample-app

Supabaseのパッケージをインストール
  1. JavaScript上でSupabaseを使うためのパッケージをインストールする。
    npm install @supabase/supabase-js

  2. Next.jsでSupabaseを使うためのヘルパーをインストールする。
    npm install @supabase/ssr

  3. CLIのパッケージもインストールする。
    npm install supabase --save-dev

    ※Supabaseのローカル開発環境を作るとき、テーブルの型定義ファイルを生成するときなどに必要。

Supabaseのプロジェクトを作成

Supabaseのアカウントがない場合は先に作成しておく。

管理画面からプロジェクトを新規作成する。

Image in a image block

するとこのような画面が表示されるのでしばらく待つ(2〜3分?)

Image in a image block

作成が終わればOK!

Image in a image block

環境変数を設定

「Home」を開いて、少し下にスクロールすると「URL」と「API Key」がある。

Image in a image block

ルートディレクトリに「.env.local」という名前の空ファイルを作成する。

そこに「URL」と「API Key」を記載する。

NEXT_PUBLIC_SUPABASE_URL=先ほど確認したURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=先ほど確認したanon Key

こんな感じ✨

Image in a image block

💡
これで環境構築が完了!

プログラム作成

基本的には公式ドキュメントのとおりでOK!

ここでは画像と補足付きで分かりやすく解説する。

①Supabaseクライアントを準備
Supabaseクライアントとは

プログラム上でSupabaseを扱うには「Supabaseクライアント」と呼ばれる変数が必要。

【手順1】Supabaseクライアントを生成する関数を作成

Supabaseクライアントを生成する汎用的な関数を作っておく。

Image in a image block

src/app/utils/supabase/server.ts(公式ドキュメントのコピペでOK)

※utils、supabaseフォルダは新規作成。

import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export const createClient = () => {
  const cookieStore = cookies();  

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: "", ...options });
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    },
  );
};

②認証で必要なミドルウェアを作成

公式ドキュメントに沿ってミドルウェアを作っていく。

ミドルウェアの必要性
  • サーバーコンポーネント

    Cookie(認証トークン) を書き込むことができない💦

  • ミドルウェア

    Cookie(認証トークン) を書き込むことができる

💡
「認証トークン」をリフレッシュするためにミドルウェアが必要。

【手順1】認証トークンをリフレッシュする関数を作成

✅ミドルウェアで使うリフレッシュ関数を準備する。

Image in a image block

src/utils/supabase/middleware.ts(公式ドキュメントのコピペでOK)

import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

// 期限切れの認証トークンをリフレッシュ
export async function updateSession(request: NextRequest) {
  // 初期のレスポンスを設定
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  })

  // Supabaseのサーバークライアントを作成
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        // クッキーを取得する関数
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        // クッキーを設定する関数
        set(name: string, value: string, options: CookieOptions) {
	        // リフレッシュした認証トークンをサーバーコンポーネントに渡す
          request.cookies.set({
            name,
            value,
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          // リフレッシュした認証トークンをブラウザに渡す
          response.cookies.set({
            name,
            value,
            ...options,
          })
        },
        // クッキーを削除する関数
        remove(name: string, options: CookieOptions) {
	        // リフレッシュした認証トークンをサーバーコンポーネントに渡す
          request.cookies.set({
            name,
            value: '',
            ...options,
          })
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          })
          // リフレッシュした認証トークンをブラウザに渡す
          response.cookies.set({
            name,
            value: '',
            ...options,
          })
        },
      },
    }
  )

  // 現在のユーザーを取得(認証トークンをリフレッシュ)
  await supabase.auth.getUser()

  // 更新されたレスポンスを返す
  return response
}

【手順2】ミドルウェアを作成

先ほど作った関数を呼び出す。

Image in a image block

src/middleware.ts(公式ドキュメントのコピペでOK)

import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'

export async function middleware(request: NextRequest) {
	// 期限切れの認証トークンをリフレッシュ
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * 以下3つのパスを除くすべてのリクエストでミドルウェアを適用する。
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * 適宜変更してもOK
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

③ログイン、サインアップ

公式ドキュメントに沿って最小限のログイン、サインアップを作っていく。

【手順1】ページ作成

✅シンプルなフォームを作るだけ。

Image in a image block

Image in a image block

src/app/login/page.tsx(公式ドキュメントのコピペでOK)

※loginフォルダは新規作成。

import { login, signup } from './actions' // まだ作っていない。後で作る。

export default function LoginPage() {
  return (
    <form>
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="password">Password:</label>
      <input id="password" name="password" type="password" required />
      
      {/* ✅Server Actionsでログイン、サインアップ */}
      <button formAction={login}>Log in</button>
      <button formAction={signup}>Sign up</button>
    </form>
  )
}

【解説】Server Actionsでログイン、サインアップ
{/* ✅Server Actionsでログイン、サインアップ */}
<button formAction={login}>Log in</button>
<button formAction={signup}>Sign up</button>

  • 「Log in」ボタンを押すとlogin関数を実行する。
  • 「Sign up」ボタンを押すとsignup関数を実行する。

💡
関数は次のステップで作る。

【手順2】ログイン、サインアップの処理(Server Actions)

ボタンを押したときの処理(ログイン、サインアップ)を作る。

Image in a image block

src/app/login/actions.ts(公式ドキュメントのコピペでOK)

'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'

/**
 * ログイン
 *
 * ログインが成功した場合はトップページへリダイレクトする。
 * ログインに失敗した場合はエラーページへリダイレクトする。
 *
 * @param formData - フォームから受け取ったデータ
 * @returns void
 */
export async function login(formData: FormData) {
	// ✅Supabaseクライアント
  const supabase = createClient()

	// フォームからデータ取得
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

	// ✅ログイン
  const { error } = await supabase.auth.signInWithPassword(data)

	// ログインエラーの場合
  if (error) {
    redirect('/error')    // 「/error」はまだ作っていない。後で作る。
  }

	// トップページのlayoutを再検証
  revalidatePath('/', 'layout')
  // トップページへリダイレクト
  redirect('/')
}

/**
 * サインアップ
 *
 * サインアップが成功した場合はトップページへリダイレクトする。
 * サインアップに失敗した場合はエラーページへリダイレクトする。
 *
 * @param formData - フォームから受け取ったデータ
 * @returns void
 */
export async function signup(formData: FormData) {
	// ✅Supabaseクライアント
  const supabase = createClient()

	// フォームからデータ取得
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

	// ✅サインアップ
  const { error } = await supabase.auth.signUp(data)

	// サインアップエラーの場合
  if (error) {
    redirect('/error')    // 「/error」はまだ作っていない。後で作る。
  }

	// トップページのlayoutを再検証
  revalidatePath('/', 'layout')
  // トップページへリダイレクト
  redirect('/')
}

【解説】Supabaseクライアント
// ✅Supabaseクライアント
const supabase = createClient()

  • ログインやサインアップをするには「Supabaseクライアント」が必要。

【解説】ログイン
// ✅ログイン
const { error } = await supabase.auth.signInWithPassword(data)

  • signInWithPassword(…)を使うだけでログインできる!
    • 引数にはログインに使うemailpasswordを指定する。

【解説】サインアップ
// ✅サインアップ
const { error } = await supabase.auth.signUp(data)

  • signUp(…)を使うだけでサインアップできる!
    • 引数には今後ログインするときに使うemailpasswordを指定する。

💡
これでプログラムの作成はほぼ終わり!

【手順3】エラー画面を作成

✅ログインや、サインアップでエラーが発生したとき用のページを作る。

Image in a image block

Image in a image block

src/app/error/page.tsx(公式ドキュメントのコピペでOK)

※errorフォルダは新規作成。

export default function ErrorPage() {
  return <p>Sorry, something went wrong</p>
}

【手順4】確認メールの内容を編集

デフォルトではサインアップするとき、入力したemail宛に確認メールが届く。

✅サーバー側で認証をするにはメールの内容を少し変更しないといけない。

💡
管理画面でポチポチするだけなのでめっちゃ簡単!

  1. 管理画面のメール編集ページへアクセスする。

    URL:https://supabase.com/dashboard/project/_/auth/templates

  2. プロジェクトを選択する。
    Image in a image block

  3. メールの内容を書き換える。
    Image in a image block

    変更前

    <h2>Confirm your signup</h2>
    
    <p>Follow this link to confirm your user:</p>
    <p><a href="{{ .ConfirmationURL }}">Confirm your mail</a></p>

    変更後(公式ドキュメントのコピペでOK)

    <h2>Confirm your signup</h2>
    
    <p>Follow this link to confirm your user:</p>
    <p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a></p>

変更点
  • aタグのhrefを"{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"に変更した。
    • {{ .SiteURL }}は、サイトのURL。
    • {{ .TokenHash }}は、確認に使うハッシュ化されたトークン。

【手順5】確認用の処理を作成

確認メールのリンク先の処理を実装する。

Image in a image block

src/app/auth/confirm/route.ts(公式ドキュメントのコピペでOK)

※auth、confirmフォルダは新規作成。

import { type EmailOtpType } from '@supabase/supabase-js'
import { type NextRequest, NextResponse } from 'next/server'

import { createClient } from '@/utils/supabase/server'

// 確認処理
export async function GET(request: NextRequest) {
  // URLからパラメータを取得
  const { searchParams } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null
  const next = searchParams.get('next') ?? '/'

  // リダイレクト先のURLを設定
  const redirectTo = request.nextUrl.clone()
  redirectTo.pathname = next
  redirectTo.searchParams.delete('token_hash')
  redirectTo.searchParams.delete('type')

  // token_hashとtypeが存在する場合
  if (token_hash && type) {
    // Supabaseクライアントを作成
    const supabase = createClient()

    // ✅OTP(ワンタイムパスワード)を検証
    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    })
    // エラーがない場合、リダイレクト先へ移動
    if (!error) {
      redirectTo.searchParams.delete('next')
      return NextResponse.redirect(redirectTo)
    }
  }

  // エラーページへリダイレクト
  redirectTo.pathname = '/error'
  return NextResponse.redirect(redirectTo)
}

【解説】OTP(ワンタイムパスワード)を検証
// ✅OTP(ワンタイムパスワード)を検証
const { error } = await supabase.auth.verifyOtp({
  type,
  token_hash,
})

  • verifyOtp(…)正規のサインアップか判定する。
    • typeが”signup”になっているか?
    • token_hashがメールに記載したハッシュ化したトークンになっているか?

動作確認

これでログイン、サインアップが使えるようになっているので実際に動かしてみる。

  1. アプリを起動
    npm run dev

  2. ページを開く

    URL:http://localhost:3000/login

    Image in a image block

  3. emailとpasswordを入力してサインアップ
    Image in a image block

  4. するとトップページにリダイレクトされる
    Image in a image block

  5. このとき確認メールが届くのでリンクをクリックする
    Image in a image block

  6. するとトップページが開く

    (内部的にはサインアップ → トップページへリダイレクト が発生)

    Image in a image block

  7. Supabaseの管理画面を見てみるとユーザーが登録されていることが分かる!
    Image in a image block

その他の処理(ログアウトなど)

✅同じく「Supabaseクライアント」を使えば簡単に実装できる。

📎
公式ドキュメントに認証関連の関数がまとまっている。

注意点

確認メールは1時間あたり3通しか送れない

テスト用のプログラムなら1時間あたり3通でも問題ないが、本番運用する場合は3通だと足りない💦

この上限は自分のSMTPサーバーを設定することで回避できる✨

(別途SMTPサーバーの契約が必要)

まとめ

今回は「メールアドレス」と「パスワード」でのログイン、サインアップを解説した。

同じような要領でGoogleなどでのソーシャルログインも簡単に作ることもできる✨

📎
公式ドキュメント(ソーシャルログイン)

ポイント
  • パッケージは「auth-helpers」ではなく「@supabase/ssr」を使う
  • 「Supabaseクライアント」と呼ばれる変数を使ってログイン、サインアップをする。

ログイン、サインアップに必要なこと
  • 認証を使うためのミドルウェアを作る。
  • emailとpasswordのフォームを作る。
  • Server Actionsにログイン、サインアップ処理を作る。

    (Server Actionsの代わりにルートハンドラなどに書いてもOK)

  • サインアップするときに送られる確認メールの内容を変更する。
  • 確認メールで使う確認処理を作る。

参考サイト