Auth(認証)

メール / OAuth / Magic Link / Phone OTP / Anonymous など主要な認証方式に対応。 セッションは JWT で、auth.uid() が RLS と直結。

Auth の中身

有効化(Dashboard)

Authentication → Providers で各プロバイダを ON / OFF。

メール + パスワード

サインアップ

const { data, error } = await supabase.auth.signUp({
  email: "alice@example.com",
  password: "********",
  options: {
    emailRedirectTo: `${origin}/auth/callback`,
    data: { display_name: "Alice" },  // user_metadata に保存
  },
})

確認メールが送信される(Dashboard で「Email confirmations」が ON のとき)。

サインイン

const { data, error } = await supabase.auth.signInWithPassword({
  email, password,
})

if (error) console.error(error.message)
console.log(data.session, data.user)

サインアウト

await supabase.auth.signOut()

Magic Link

await supabase.auth.signInWithOtp({
  email,
  options: { emailRedirectTo: `${origin}/auth/callback` },
})

ユーザがメールのリンクをクリックすると /auth/callback?code=... 付きで飛ぶ。 サーバ側で exchangeCodeForSession を呼んでセッション確立。

OAuth(Google など)

Provider 設定

  1. Google Cloud Console / GitHub OAuth Apps で OAuth クライアントを作る
  2. Redirect URLhttps://<project>.supabase.co/auth/v1/callback
  3. Client ID / Secret を Supabase Dashboard に貼る

クライアントから呼ぶ

await supabase.auth.signInWithOAuth({
  provider: "google",
  options: {
    redirectTo: `${origin}/auth/callback`,
    scopes: "email profile",
  },
})

Phone OTP(SMS)

// 送信
await supabase.auth.signInWithOtp({ phone: "+819012345678" })

// 検証
await supabase.auth.verifyOtp({
  phone: "+819012345678",
  token: "123456",
  type: "sms",
})

Twilio / MessageBird / Vonage などの SMS プロバイダを Dashboard で設定する必要あり。

Anonymous Sign-in

ログイン不要だがユーザ IDを持たせたい場合。匿名 → 後でメール紐付け可能。

const { data } = await supabase.auth.signInAnonymously()
// data.user.id が振られる

// あとで本登録に昇格
await supabase.auth.updateUser({ email: "alice@example.com", password: "..." })

セッションの取得

// 現在のセッション
const { data: { session } } = await supabase.auth.getSession()

// 現在のユーザ
const { data: { user } } = await supabase.auth.getUser()

console.log(user?.id, user?.email)
getSession() vs getUser()

getSession()クライアント側のキャッシュを返すので偽装可能。 サーバで認可判断するときは必ず getUser()(サーバ側で署名検証する)。

セッション変化を監視

const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    // event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | ...
    if (event === "SIGNED_IN") refreshUI(session.user)
  }
)

// 解除
subscription.unsubscribe()

JWT の中身

Supabase の access_token を jwt.io でデコードすると:

{
  "aud": "authenticated",
  "exp": 1731555555,
  "iat": 1731551955,
  "sub": "uuid-of-user",
  "email": "alice@example.com",
  "role": "authenticated",
  "user_metadata": { "display_name": "Alice" },
  "app_metadata": { "provider": "email" }
}

パスワードリセット

// メール送信
await supabase.auth.resetPasswordForEmail(email, {
  redirectTo: `${origin}/auth/reset`,
})

// 新パスワード設定(リダイレクト先で)
await supabase.auth.updateUser({ password: newPassword })

メール変更

await supabase.auth.updateUser({ email: "new@example.com" })
// 旧 / 新両方に確認メール

MFA(多要素認証)

// TOTP の登録
const { data } = await supabase.auth.mfa.enroll({ factorType: "totp" })
// data.totp.qr_code をユーザに表示

// チャレンジ
const { data: challenge } = await supabase.auth.mfa.challenge({ factorId })

// 検証
await supabase.auth.mfa.verify({
  factorId,
  challengeId: challenge.id,
  code: "123456",
})

Next.js App Router での認証フロー

サインインボタン(Client Component)

"use client"
import { supabase } from "@/lib/supabase/client"

export function SignInButton() {
  return (
    <button onClick={() => supabase.auth.signInWithOAuth({
      provider: "google",
      options: { redirectTo: `${location.origin}/auth/callback` },
    })}>
      Sign in with Google
    </button>
  )
}

OAuth コールバック(Route Handler)

// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server"
import { NextResponse } from "next/server"

export async function GET(req: Request) {
  const url = new URL(req.url)
  const code = url.searchParams.get("code")
  if (code) {
    const supabase = await createClient()
    await supabase.auth.exchangeCodeForSession(code)
  }
  return NextResponse.redirect(new URL("/", req.url))
}

保護したいページ(Server Component)

// app/dashboard/page.tsx
import { createClient } from "@/lib/supabase/server"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) redirect("/login")

  return <div>Welcome, {user.email}</div>
}

RLS と auth.uid()

ログイン中のユーザ ID を SQL から参照できる:

create policy "user can read own posts"
  on posts for select
  to authenticated
  using (auth.uid() = author_id);

詳細は RLS ページで。

カスタムクレーム / ロール管理

「管理者だけアクセスできるテーブル」のような場合、app_metadata.role"admin" を入れて、 SQL から参照する。

-- カスタム関数で取得
create or replace function auth.is_admin()
returns boolean
language sql security definer
as $$
  select coalesce(
    (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin',
    false
  )
$$;

app_metadata はservice_role でしか書けないので安全。

セキュリティ Tips

料金 / 制限

失敗パターン

症状原因
OAuth 後にユーザがログインできないRedirect URL の登録ミス
auth.uid() が nullRLS で to authenticated が抜けている / セッション無し
token 期限切れrefresh する仕組みが無い → middleware で getUser を呼ぶ
Email Confirmation 来ないSMTP 設定 / レート制限 / 迷惑メール
onAuthStateChange が複数回1 リスナで足りる、unsubscribe を忘れずに

Auth Helpers / SSR

原則

Supabase Auth は「どのユーザがアクセスしているか」を JWT で判別する仕組み。 ここに RLS が組み合わさることでサーバレス的に安全な認可が実現される。 Auth + RLS はセットで覚える。