Pasang Google Sign-In di Next.js + Supabase (Sign In & Sign Up Sekaligus)

Pasang Google Sign-In di Next.js + Supabase (Sign In & Sign Up Sekaligus)

Konsep Dulu (Biar Gak Ngikutin Buta)

Analogi: Google OAuth itu kayak masuk gedung pakai kartu akses kantor lain yang sudah dipercaya satpam.

  1. User klik "Continue with Google" → app melempar user ke Google ("satpam, orang ini mau masuk, tolong verifikasi").
  2. Google minta user pilih akun + consent → Google melempar balik ke Supabase bawa bukti (code).
  3. Supabase tukar code jadi session (cookie login) → lempar user balik ke app kamu.

Yang menarik: sign up dan sign in itu SATU flow yang sama. Kalau emailnya belum ada → Supabase otomatis bikin user baru. Kalau sudah ada (dan verified) → login ke user itu. Gak perlu bikin halaman "Daftar pakai Google" terpisah.

Flow teknisnya bernama PKCE — sudah di-handle penuh oleh @supabase/ssr, kita tinggal pasang 3 potong: tombol → callback route → middleware allowlist.

Bagian 1 — Setup Google Cloud Console (Manual, Sekali)

  1. Buka console.cloud.google.com → buat/pilih project.
  2. APIs & Services → OAuth consent screen (kalau baru):
    • User type: External
    • Isi nama app + support email. Scopes: skip (default email, profile, openid cukup).
    • Tambahkan email sendiri sebagai Test user.
    • ⚠️ Status Testing = cuma test user yang bisa login (max 100). Klik Publish app kalau mau semua orang bisa. Scope basic gak butuh proses verifikasi Google.
  3. APIs & Services → Credentials → Create Credentials → OAuth client ID:
    • Application type: Web application
    • Authorized JavaScript origins: http://localhost:3000 (+ domain production nanti)
    • Authorized redirect URIs: https://<project-ref>.supabase.co/auth/v1/callback
      • ⚠️ Ini URL Supabase, BUKAN URL app kamu. Google melempar ke Supabase dulu, bukan langsung ke app.
    • Simpan Client ID + Client Secret.

Bagian 2 — Setup Supabase Dashboard (Manual, Sekali)

  1. Authentication → Sign In / Providers → Google: toggle Enable, paste Client ID + Secret, Save.
    • Gejala kalau lupa: error 400 — provider is not enabled (kejadian 😅 — kelihatan di Logs → Auth).
  2. Authentication → URL Configuration → Redirect URLs: tambahkan http://localhost:3000/auth/callback.
    • Ini daftar alamat yang BOLEH jadi tujuan redirect setelah login. Gak terdaftar → user dilempar ke Site URL default, login bisa nyasar/gagal.
    • Production nanti: tambah https://<domain>/auth/callback.

Bagian 3 — Kode (4 Potong)

3a. Server Action — pelempar ke Google

// app/(auth)/signin/actions.ts
"use server";
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { createClient } from "@/lib/supabase/server";

export async function signInWithGoogle() {
  const supabase = await createClient();
  const origin = (await headers()).get("origin");
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: { redirectTo: `${origin}/auth/callback` },
  });

  if (error || !data.url) {
    redirect("/signin?message=Could+not+sign+in+with+Google");
  }

  redirect(data.url); // redirect eksternal ke Google consent screen
}

redirectTo = ke mana Supabase melempar user SETELAH sukses (callback app kita). origin diambil dari header biar jalan di localhost maupun production tanpa hardcode.

3b. Tombol — komponen shared (dipakai di signin DAN signup)

// app/(auth)/_components/GoogleButton.tsx
import { signInWithGoogle } from "../signin/actions";

export default function GoogleButton() {
  return (
    <form action={signInWithGoogle}>
      <button type="submit" className="...">
        {/* SVG logo Google 4 warna */}
        Continue with Google
      </button>
    </form>
  );
}

Cukup <form action={serverAction}> — gak perlu client-side JS, gak perlu library tambahan. Pasang di halaman signin dan signup (komponennya sama, karena flow-nya memang sama).

3c. Callback route — penukar code jadi session

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

export async function GET(request: NextRequest) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) return NextResponse.redirect(`${origin}/`);
  }

  return NextResponse.redirect(`${origin}/signin?message=Authentication+failed`);
}

Bonus: route ini juga dipakai email verification (link konfirmasi signup email/password) — satu callback untuk semua.

3d. Middleware — JANGAN LUPA, INI JEBAKAN PALING SERING

Kalau middleware kamu redirect semua path tanpa session ke /signin, request callback bakal ke-blokir sebelum sempat menukar code (saat callback datang, session memang BELUM ada — itu justru tujuannya). Allowlist:

// lib/supabase/middleware.ts
const publicPaths = ["/signin", "/signup", "/auth/callback"];
if (!user && !publicPaths.some((p) => request.nextUrl.pathname.startsWith(p))) {
  // redirect ke /signin
}

Gejala kalau lupa: klik Google → consent sukses → balik ke app malah mental ke halaman signin lagi, gak pernah login.

Bagian 4 — Profil User Otomatis (Trigger DB)

Kalau app punya tabel public.user_profiles (mirror dari auth.users), pakai trigger biar user Google baru otomatis dapat row profil — termasuk nama dan foto dari Google:

create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path to 'public'
as $$
begin
  insert into public.user_profiles (id, email, display_name, avatar_url)
  values (
    new.id,
    new.email,
    coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email, '@', 1)),
    new.raw_user_meta_data->>'avatar_url'  -- foto profil Google
  );
  return new;
end; $$;

create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();

Google mengirim full_name dan avatar_url di raw_user_meta_data — tinggal copy.

Bonus: User Lama (Email/Password) Login Pakai Google?

Bisa, otomatis. Kalau email akun Google sama persis dengan email user existing yang sudah verified → Supabase auto-link identity Google ke user itu. User ID sama, data aman, gak dobel. Habis itu user bisa login dua cara: password ATAU Google.

Syarat: emailnya harus match persis (kejadian nyata: email di DB typo gmai.com → gak match → harus dibetulkan dulu di auth.users + auth.identities + tabel profil).

Debugging Cheat Sheet

GejalaPenyebab
400 provider is not enabled (Supabase auth logs)Provider Google belum di-enable di dashboard
redirect_uri_mismatch (halaman Google)Redirect URI di Google Console salah/typo — harus https://<ref>.supabase.co/auth/v1/callback
access_denied (halaman Google)Consent screen masih Testing + akun bukan test user
Consent sukses tapi balik ke signin, gak login/auth/callback gak di-allowlist middleware
Login sukses tapi nyasar/gagal redirectURL callback belum ada di Redirect URLs (Supabase URL Configuration)
User baru gak punya row profilTrigger handle_new_user belum ada / error

Cara lihat log: Supabase Dashboard → Logs → Auth. Error provider is not enabled dkk kelihatan jelas di situ.

Ringkasan

  1. Google Cloud: OAuth client ID (Web) → redirect URI = URL Supabase.
  2. Supabase: enable provider + isi Redirect URLs.
  3. Kode: server action signInWithOAuth → tombol form → callback exchangeCodeForSessionallowlist middleware.
  4. Trigger DB buat auto-profil (nama + avatar Google).
  5. Sign up = sign in = satu tombol. Auto-link ke user lama kalau email match.

Total kode ~120 baris, zero library tambahan di luar @supabase/ssr yang memang sudah dipakai.