メインコンテンツへスキップ
Think You Lab
ブログログイン無料で始める
トップ/ブログ/how-to
SupabaseNext.js認証magic linkApp Router

Supabase Auth で magic link ログインを実装する — Next.js App Router 対応

Supabase の magic link(パスワードレス)ログインを Next.js App Router で実装する手順を解説。signInWithOtp・/auth/callback・Server Action の実装パターンを、実際のプロダクションコードをもとに説明します。

2026-04-15·約15分

LINE 公式アカウント

AI副業スタートガイドをLINEで無料配布中

目次
  • magic link ログインの仕組み
  • Supabase の設定(ダッシュボード側)
  • Email Provider を有効化する
  • Site URL と Redirect URLs の設定
  • Next.js 側の実装
  • `@supabase/ssr` のインストールと `utils/supabase/server.ts`
  • Server Action で `signInWithOtp` を呼ぶ
  • `/auth/callback/route.ts` で `exchangeCodeForSession`
  • フロントエンドのフォーム
  • ハマりポイントと対処法
  • リダイレクト先が localhost になる
  • Cookie が正しくセットされない
  • "Invalid redirect URL" エラー
  • Google OAuth との組み合わせ
  • まとめ

目次

  • magic link ログインの仕組み
  • Supabase の設定(ダッシュボード側)
  • Email Provider を有効化する
  • Site URL と Redirect URLs の設定
  • Next.js 側の実装
  • `@supabase/ssr` のインストールと `utils/supabase/server.ts`
  • Server Action で `signInWithOtp` を呼ぶ
  • `/auth/callback/route.ts` で `exchangeCodeForSession`
  • フロントエンドのフォーム
  • ハマりポイントと対処法
  • リダイレクト先が localhost になる
  • Cookie が正しくセットされない
  • "Invalid redirect URL" エラー
  • Google OAuth との組み合わせ
  • まとめ

パスワードレスログインを実装したいが、何から手を付ければいいか分からない。

そう感じているなら、この記事で解決します。

magic link とは、ユーザーがメールアドレスを入力するとワンタイムリンクが届き、そのリンクをクリックするだけでログインできる仕組みです。パスワードの設定・管理が不要で、ユーザー体験がシンプルになります。

この記事で実装する流れ:

  1. Supabase ダッシュボードで Email Provider を有効化する
  2. Server Action(signInWithOtp)でメールを送る
  3. /auth/callback ルートでコードをセッションに変換する

THINK YOU LAB でも同じスタック(Next.js App Router + @supabase/ssr + magic link)で本番稼働しています。この記事のコードは実際のプロダクションコードから引用しています。

Supabase 入門ガイドはこちら


magic link ログインの仕組み

ユーザーがメールアドレスを入力
  ↓ signInWithOtp を呼ぶ
  ↓ Supabase が magic link メールを送信
  ↓ ユーザーがリンクをクリック
  ↓ ブラウザが /auth/callback?code=XXXX にリダイレクト
  ↓ exchangeCodeForSession でセッション確立
  ↓ Cookie にセッションが保存される
  ↓ /member/onboarding にリダイレクト(ログイン完了)

Supabase Auth は PKCE(Proof Key for Code Exchange)フローを使います。signInWithOtp を呼ぶとブラウザ側でコードチャレンジが生成され、Supabase 側に保存されます。ユーザーがリンクをクリックして /auth/callback に戻ってきたとき、exchangeCodeForSession がコードとチャレンジを照合してセッションを発行します。


Supabase の設定(ダッシュボード側)

Email Provider を有効化する

  1. 左メニューから「Authentication」→「Providers」を開く
  2. 「Email」を展開して「Enable Email Provider」をオンにする
  3. 「Confirm email」を有効にする
  4. 「Save」をクリック

Site URL と Redirect URLs の設定

magic link のコールバック先 URL を Supabase 側に登録します。登録がないと「Invalid redirect URL」エラーで認証が失敗します。

「Authentication」→「URL Configuration」で設定:

| 設定項目 | 値の例 | |---|---| | Site URL | https://think-you-lab.vercel.app | | Redirect URLs(本番) | https://think-you-lab.vercel.app/** | | Redirect URLs(ローカル) | http://localhost:3000/** |

ワイルドカード(**)を使うと、/auth/callback?next=... のようなクエリパラメータ付きの URL もすべて許可されます。


Next.js 側の実装

@supabase/ssr のインストールと utils/supabase/server.ts

npm install @supabase/supabase-js @supabase/ssr

サーバーサイドで Supabase クライアントを生成する utils/supabase/server.ts:

import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
 
export async function createClient() {
  const cookieStore = await cookies();
 
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Server Component から呼ばれた場合は無視(middleware が担当)
          }
        },
      },
    }
  );
}

setAll の try/catch が重要です。Server Component(読み取り専用コンテキスト)から呼ばれた場合に Cookie への書き込みが失敗しますが、エラーを握りつぶすことで同じ createClient() をどこからでも安全に呼べるようにしています。

クライアントサイドでは utils/supabase/client.ts を使います:

import { createBrowserClient } from "@supabase/ssr";
 
export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

Server Action で signInWithOtp を呼ぶ

// lms/app/(public)/login/actions.ts
"use server";
 
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
 
export async function signInWithMagicLink(formData: FormData) {
  const email = formData.get("email") as string;
  const next = (formData.get("next") as string) || "/member/onboarding";
 
  const supabase = await createClient();
  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback?next=${encodeURIComponent(next)}`,
    },
  });
 
  if (error) {
    redirect(`/login?error=${encodeURIComponent(error.message)}&next=${encodeURIComponent(next)}`);
  }
 
  redirect(`/login/check-email?email=${encodeURIComponent(email)}`);
}

重要: emailRedirectTo は絶対 URLで指定してください。相対パスは動作しません。Vercel を使っている場合は NEXT_PUBLIC_SITE_URL 環境変数に本番 URL を設定しておきます。

ロジックの流れ:

  1. formData からメールアドレスと遷移先(next)を取得
  2. signInWithOtp でメールを送信
  3. 送信後は /login/check-email にリダイレクトして「メールを確認してください」と表示

/auth/callback/route.ts で exchangeCodeForSession

// lms/app/auth/callback/route.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse, type NextRequest } from "next/server";
 
function sanitizeNext(next: string | null) {
  return next && next.startsWith("/") ? next : "/member/onboarding";
}
 
export async function GET(request: NextRequest) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  const next = sanitizeNext(searchParams.get("next"));
 
  if (!code) {
    const message = searchParams.get("error_description") ?? "ログインリンクの確認に失敗しました。";
    return NextResponse.redirect(`${origin}/login?error=${encodeURIComponent(message)}`);
  }
 
  const cookieStore = await cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll(); },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
        },
      },
    }
  );
 
  const { error } = await supabase.auth.exchangeCodeForSession(code);
 
  if (error) {
    return NextResponse.redirect(`${origin}/login?error=${encodeURIComponent(error.message)}`);
  }
 
  return NextResponse.redirect(`${origin}${next}`);
}

sanitizeNext() でオープンリダイレクト脆弱性を防いでいます:

  • / で始まる値 → そのまま通過(内部パス)
  • 外部 URL(https://evil.com など)→ /member/onboarding に差し替え

フロントエンドのフォーム

// lms/app/(public)/login/page.tsx
import { signInWithMagicLink } from "./actions";
 
export default function LoginPage({ searchParams }: { searchParams: { next?: string } }) {
  const next = searchParams.next || "/member/onboarding";
 
  return (
    <form action={signInWithMagicLink}>
      <input type="email" name="email" placeholder="メールアドレス" required />
      <input type="hidden" name="next" value={next} />
      <button type="submit">メールでログイン</button>
    </form>
  );
}

form の action に Server Action を渡すだけで動作します。クライアントサイドの JavaScript や useState は不要です。


ハマりポイントと対処法

リダイレクト先が localhost になる

症状: 本番環境でリンクをクリックすると http://localhost:3000/auth/callback に飛ばされる

原因: emailRedirectTo に http://localhost:3000 が指定されている

対処: NEXT_PUBLIC_SITE_URL 環境変数を本番 URL に設定して emailRedirectTo に使う。Vercel では Settings → Environment Variables に追加します。

Cookie が正しくセットされない

症状: /auth/callback でコード交換が成功しているのに、その後 getUser() が null を返す

原因: createServerClient に渡す cookies オブジェクトに setAll が実装されていない

対処: route.ts の setAll が正しく実装されているか確認。setAll がないと Supabase はセッション Cookie を保存できません。

"Invalid redirect URL" エラー

症状: magic link をクリックすると Supabase から Invalid redirect URL エラーが返る

原因: Supabase ダッシュボードの Redirect URLs にコールバック URL が登録されていない

対処: 「Authentication」→「URL Configuration」→「Redirect URLs」に本番 URL とローカル URL の両方を登録する。


Google OAuth との組み合わせ

magic link と Google OAuth を並列で提供する場合、signInWithGoogle を別の Server Action として定義します:

export async function signInWithGoogle(formData: FormData) {
  const next = (formData.get("next") as string) || "/member/onboarding";
  const supabase = await createClient();
 
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback?next=${encodeURIComponent(next)}`,
    },
  });
 
  if (error || !data.url) redirect(`/login?error=${encodeURIComponent(error?.message ?? "OAuth error")}`);
  redirect(data.url);
}

コールバック URL は magic link と同じ /auth/callback を使います。Google OAuth も PKCE フローを経由するため、exchangeCodeForSession で同様にセッション確立できます。

フロントエンドのボタン追加は1行のフォームだけ:

<form action={signInWithGoogle}>
  <input type="hidden" name="next" value={next} />
  <button type="submit">Google でログイン</button>
</form>

まとめ

| 項目 | 実装場所 | ポイント | |---|---|---| | Email Provider 有効化 | Supabase ダッシュボード | Confirm email もオンに | | Redirect URLs 登録 | Supabase URL Configuration | 本番・ローカル両方登録 | | メール送信 | Server Action(signInWithOtp) | emailRedirectTo は絶対 URL | | セッション確立 | /auth/callback/route.ts | exchangeCodeForSession | | オープンリダイレクト防止 | sanitizeNext() | / で始まらない値を拒否 |

次のステップ:

  • ログイン後のページ保護(middleware による認証ガード)
  • Supabase 入門ガイド

「認証の実装で詰まって前に進めない」方向けに、Supabase Magic Link 認証の実装テンプレ(Next.js App Router 対応)を LINE で無料配布中です。

→ Supabase 認証テンプレ(Next.js 対応)を受け取る(LINE登録・無料)

LINE 公式アカウント

Supabase Auth 実装チートシート・magic link設定手順をLINEで配布中

R

Rikuto (LAB)

非エンジニアが Claude Code × n8n × Supabase で副業システムを作り続ける実験記。 失敗も含めたリアルな一次情報を発信しています。

THINK YOU LAB 運営

関連記事

  • Next.js App Router の middleware で Supabase 認証ガードを作る — 実装コード全公開

    2026-04-15

  • Stripe Checkout を Next.js Server Action で実装する — Supabase Auth 連携つき

    2026-04-15

  • Supabase + Stripe Webhook で会員ステータスを自動更新する — Next.js App Router 実装

    2026-04-15

← ブログ一覧へ
X でシェアLINE でシェア
← 前の記事Supabase RLS(行レベルセキュリティ)の書き方入門 — 実テーブルのポリシー例つき
次の記事 →Supabase + Stripe Webhook で会員ステータスを自動更新する — Next.js App Router 実装

目次

  • magic link ログインの仕組み
  • Supabase の設定(ダッシュボード側)
  • Email Provider を有効化する
  • Site URL と Redirect URLs の設定
  • Next.js 側の実装
  • `@supabase/ssr` のインストールと `utils/supabase/server.ts`
  • Server Action で `signInWithOtp` を呼ぶ
  • `/auth/callback/route.ts` で `exchangeCodeForSession`
  • フロントエンドのフォーム
  • ハマりポイントと対処法
  • リダイレクト先が localhost になる
  • Cookie が正しくセットされない
  • "Invalid redirect URL" エラー
  • Google OAuth との組み合わせ
  • まとめ
Think You Lab
このブログについて料金プランプライバシーポリシーお問い合わせ特定商取引法ログイン

© 2026 Think You Lab