Supabase OAuth 社群登入:Google/GitHub 一鍵登入實作 | Supabase 完整教學

2026/10/10
Supabase OAuth 社群登入:Google/GitHub 一鍵登入實作 | Supabase 完整教學

上一篇《Email/Password 驗證》我們讓使用者用 email 加密碼登入。這一篇換一種對使用者更省事的進門方式:OAuth 社群登入——按一顆「用 Google 登入」或「用 GitHub 登入」的按鈕就完成。我們會走完整條路:在 Dashboard 設定 provider 的 client id/secret 與 callback URL、前端呼叫 signInWithOAuth 觸發跳轉、理解 PKCE 流程如何防截取、在 callback route 用 exchangeCodeForSession 換回 session,最後讀取 provider 帶回的個人資料。

前言

一句話定義本篇主題:OAuth 社群登入是讓使用者「用既有的 Google/GitHub 帳號」登入你的應用——你不儲存密碼,改由 provider 驗證身份。前端呼叫 signInWithOAuth({ provider }) 把使用者重定向到 provider 授權頁,使用者同意後帶著一個 code 導回你的 callback,你在 callback 用 exchangeCodeForSession(code) 換回一組 session;整個交換過程由 PKCE 流程保護,登入後 provider 的個人資料會進到 user_metadata。

延續上一篇反覆強調的那句話:無論用哪種登入方式,最終產物都是同一組 session(access token + refresh token)。OAuth 只是換了「驗證身份」這一段的做法——不是你的 email 密碼,而是 Google/GitHub 幫你確認「這個人是誰」。一旦繞完那一圈拿到 session,後面的 getUser()、RLS 整合、onAuthStateChange 全都和上一篇一模一樣。所以你只需要把注意力放在「OAuth 特有的那段跳轉與交換」。

用一個類比幫你抓住全貌:OAuth 社群登入就像用「政府核發的身分證」進入一棟辦公大樓。 你不需要為這棟樓另外辦一張門禁卡(不用註冊新密碼),而是走到櫃台(按下「用 Google 登入」),把身分證交給大樓合作的驗證單位(provider,Google)查驗;驗證單位確認你是本人後,發給你一張「今日臨時通行證」(code),你拿這張通行證到大樓門禁系統換一張正式門禁卡(exchangeCodeForSession 換 session)。而 PKCE 就像你交出身分證時,同時在櫃台留下一個只有你知道的暗號——就算通行證半路被偷,小偷沒有那個暗號,也換不到門禁卡。

本篇你會學到:

  • provider 設定:如何在 Google Cloud/GitHub 建立 OAuth app,拿到 client id/secret,並把 Supabase 的 callback URL 填對
  • 觸發登入:signInWithOAuth({ provider, options: { redirectTo } }) 的行為與 scopes、queryParams
  • PKCE 與交換:為什麼要 PKCE、callback route 如何用 exchangeCodeForSession(code) 拿回 session
  • provider 資料:登入後從 user_metadata 讀個人資料、以及 provider_token 的取用時機
  • 安全底線:callback/redirect URL 為何要精確設定、client secret 為何不能外洩、user_metadata 為何不可信任

核心概念

OAuth 登入的整條路徑,可以濃縮成一句話:前端觸發跳轉 → provider 授權 → 帶 code 導回 callback → 用 code 換 session。先把這條線在腦中畫清楚,實作就只是把每一段接上對應的設定與 API。

一、OAuth 授權碼流程(含 PKCE)示意

先看整條路徑長什麼樣。這是走 PKCE 的授權碼流程(Authorization Code Flow),也是 SSR 環境的標準做法:

【你的網站】使用者按「用 Google 登入」
      │
      ├─ 前端呼叫 signInWithOAuth({ provider: 'google', options: { redirectTo } })
      │   SDK 在本地產生 code_verifier + code_challenge(SHA-256)
      │
      └─ 瀏覽器被重定向到 Google 授權頁(帶著 code_challenge)
                 │
                 └─【Google】使用者登入 Google、按下「同意授權」
                            │
                            └─ Google 導回 Supabase 的 callback URL
                               (https://<ref>.supabase.co/auth/v1/callback)
                                       │
                                       └─ Supabase 再導回你設定的 redirectTo
                                          (例如 /auth/callback?code=xxxx)
                                                  │
                                                  └─【你的 callback route】
                                                     exchangeCodeForSession(code)
                                                     (用 code + code_verifier 換 session)
                                                             │
                                                             └─ 拿到 session → 使用者正式登入

注意其中的兩段重定向:Google 先導回「Supabase 的」callback(那是你在 Google Cloud 設定的固定網址),Supabase 處理完再導回「你的」redirectTo。很多人把這兩個網址搞混,是設定卡關的頭號原因——後面會專門講。

二、provider 設定:三個必填、一個固定

無論是 Google、GitHub 或其他 provider,Dashboard 設定的心智模型都一樣:你要在 provider 那邊建立一個 OAuth application,拿到一組 client id 與 client secret,貼進 Supabase;同時把 Supabase 的 callback URL 填進 provider 的允許清單。

項目從哪裡拿填到哪裡
Client IDprovider(Google Cloud/GitHub)建立 OAuth app 後產生Supabase Dashboard → Authentication → Providers
Client Secret同上(機密,等同密碼)同上(Supabase 保管,勿外流)
Callback URLSupabase 固定提供:https://<project-ref>.supabase.co/auth/v1/callback貼進 provider 的「Authorized redirect URIs」
redirectTo你自己網站的 callback 頁面,例如 https://example.com/auth/callback前端 signInWithOAuth 的 options + Supabase 的 URL Allowlist

這裡最關鍵的觀念:那個 .../auth/v1/callback 是「Supabase 的」callback,不是「你網站的」。 provider(Google)只認得這個 Supabase 網址;使用者授權後 Google 先導回這裡,Supabase 處理完才導回你自己的 redirectTo。所以在 Google Cloud 的「Authorized redirect URIs」欄位要填的是 Supabase 網址,不是 example.com。

三、PKCE:防止授權碼被半路盜用

code(授權碼)在網址上明碼傳遞,理論上可能被攔截。PKCE(Proof Key for Code Exchange) 就是為了讓「攔到 code 也沒用」而設計:

  • 登入發起時,SDK 在瀏覽器本地產生隨機的 code_verifier,並算出它的 SHA-256 雜湊 code_challenge。
  • 只有 code_challenge 被送到 provider;code_verifier 留在你的瀏覽器。
  • 換 session 時(exchangeCodeForSession)要同時送出 code 與 code_verifier,provider 端驗證兩者匹配才發 session。
  • 攻擊者就算在網址上攔到 code,沒有留在你瀏覽器裡的 code_verifier,也換不出 session。

@supabase/ssr 套件預設就走 PKCE,你不需要自己產生 verifier;你要做的只是「提供一個 callback route 呼叫 exchangeCodeForSession」把最後一步接上。

值得對照的是舊的 implicit flow(隱式流程):它不繞授權碼這一步,而是讓 provider 直接把 access token 放進重定向網址的 hash 片段(#access_token=...)回傳。這種做法在純前端 SPA 曾經常見,但風險較高——token 可能殘留在瀏覽器歷史、Referer header 或伺服器 log 裡。兩者的差別可以這樣記:

流程回傳的東西誰換 session安全性適用
PKCE(授權碼)一次性 code你的 callback 端 exchangeCodeForSession高(有 code_verifier 護體)SSR、現代做法(推薦)
Implicit(隱式)直接是 access token(在 hash)SDK 自動從 hash 解析較低純前端、無 callback route

結論很簡單:能走 PKCE 就走 PKCE。用 @supabase/ssr 時它是預設;純前端沒有 callback route 時,SDK 才會退回 implicit flow 自動解析 hash。

四、關鍵術語小抄

術語意義
provider第三方身份提供者:'google'、'github'、'apple'…
signInWithOAuth前端 API,觸發「重定向到 provider 授權頁」
Callback URLprovider 授權後回呼的網址(Supabase 固定的 .../auth/v1/callback)
redirectToSupabase 處理完後,導回你自己網站的網址
code授權碼,provider 回傳、用來換 session 的一次性憑證
exchangeCodeForSession在 callback route 用 code 換回 session
scopes向 provider 要求的權限範圍(如讀 email、讀 repo)
provider_tokenprovider 發的 access token,可拿去呼叫 provider 自己的 API

實作範例

以下用 supabase-js(TypeScript)+ Next.js App Router 情境示範,這是最常見、也最需要 callback route 的組合。我們依「設定 → 觸發 → 交換 → 讀資料」的順序逐段實作。

一、Dashboard/provider 設定要點

程式碼之前,先把設定做對,否則怎麼寫都登不進去。以 GitHub 為例(Google 概念相同,只是在 Google Cloud Console 操作):

  1. 到 GitHub → Settings → Developer settings → OAuth Apps → New OAuth App。
  2. Authorization callback URL 填 Supabase 的固定 callback:https://<project-ref>.supabase.co/auth/v1/callback。
  3. 建立後拿到 Client ID,並產生一組 Client Secret。
  4. 回 Supabase Dashboard → Authentication → Providers → GitHub → 貼入 Client ID/Secret → Enable → Save。
  5. 到 Authentication → URL Configuration,把你自己網站的 callback(如 https://example.com/auth/callback)加進 Redirect URL Allowlist。

本地開發若用 Supabase CLI,可在 supabase/config.toml 設定,secret 用環境變數帶入(不要把 secret 寫死進版控):

# supabase/config.toml
[auth.external.github]
enabled = true
client_id = "your-github-client-id"
# 用 env() 從環境變數讀取,避免 secret 進 git
secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_SECRET)"

Google 的設定差異值得單獨提一下。Google 需要在 Google Cloud Console 建立專案,設定 OAuth 同意畫面(consent screen),再到「憑證(Credentials)」建立 Web application 類型的 OAuth Client。同樣把 Supabase 的 .../auth/v1/callback 填進「已授權的重新導向 URI」,然後複製 Client ID 與 Secret 回貼 Supabase。Google 還有一個 GitHub 沒有的特色:支援 One Tap 與 ID Token 流程(signInWithIdToken),能在不離開頁面的情況下登入——不過那屬於進階用法,本篇聚焦標準重定向流程即可。設定的心智模型對所有 provider 都一致:provider 端建立 app、拿 client id/secret、填 Supabase 的 callback;Supabase 端貼 id/secret、啟用;你網站的 redirectTo 進 Allowlist。 記住這條,你就能自己接上 Discord、Apple、Microsoft Azure 等任何 provider。

二、建立 Supabase Client(SSR)

SSR 情境用 @supabase/ssr,它預設啟用 PKCE。以下是伺服器端 client 工具函式:

// 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 無法寫 cookie,交給 Middleware 處理
          }
        },
      },
    }
  )
}

前端觸發登入則用瀏覽器端 client:

// 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!
  )
}

三、觸發 OAuth 登入:signInWithOAuth

前端按鈕觸發。重點是 redirectTo 要指向你自己的 callback route,而且這個網址必須在 Allowlist 裡:

'use client'
import { createClient } from '@/utils/supabase/client'

async function signInWithGitHub() {
  const supabase = createClient()

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      // Supabase 處理完後導回這裡(必須在 Redirect URL Allowlist)
      redirectTo: `${window.location.origin}/auth/callback`,
    },
  })

  if (error) {
    console.error('OAuth 觸發失敗:', error.message)
    return
  }

  // 注意:此時「還沒登入」,data 只含要跳轉的 url
  // supabase-js 預設會自動把瀏覽器導向 data.url
  // 若你想手動控制跳轉,可設 options.skipBrowserRedirect: true 再用 data.url
}

一定要理解:這裡的 data 沒有 session。signInWithOAuth 的產物是「一個要跳轉的網址」,session 要等使用者繞完 provider、回到 callback 才拿得到。

若要指定 Google 並要求特定 scopes(例如要讀取更多個人資料),用 scopes;要拿 Google 的 refresh token(持久呼叫 Google API)則用 queryParams:

async function signInWithGoogle() {
  const supabase = createClient()

  await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
      // 向 Google 要求額外權限範圍(空白分隔)
      scopes: 'openid email profile',
      // 要拿到 Google 的 refresh token(持久存取 Google API)時才需要
      queryParams: {
        access_type: 'offline',
        prompt: 'consent',
      },
    },
  })
}

四、callback route:用 exchangeCodeForSession 換回 session

這是 OAuth 最關鍵、也最常被漏掉的一步。使用者從 provider 繞回來時,Supabase 會導向 /auth/callback?code=xxxx,你要在這個 Route Handler 用 code 換 session(PKCE 交換):

// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'

export async function GET(request: NextRequest) {
  const { searchParams, origin } = new URL(request.url)
  // provider 導回時帶的授權碼
  const code = searchParams.get('code')
  // 登入完成後想去的頁面(可選)
  const next = searchParams.get('next') ?? '/'

  if (code) {
    const supabase = await createClient()
    // 用 code(+瀏覽器裡的 code_verifier)換回 session,並寫進 cookie
    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      // 成功:session 已寫入 cookie,導向目標頁
      return NextResponse.redirect(`${origin}${next}`)
    }
  }

  // 沒有 code、或交換失敗 → 導到錯誤頁
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}

exchangeCodeForSession(code) 成功後,session 就被寫進 HTTP-only cookie,之後的 Server Component、Middleware 都能透過 getUser() 讀到已登入的使用者——和上一篇 Email/Password 登入後完全一致。這正是那句老話的體現:出口一致,換的只是入口。你在上一篇為 Email/Password 寫好的 Middleware、受保護頁面、getUser() 邏輯,OAuth 完全共用,一行都不用改。

如果你是純前端 SPA(沒有伺服器端 callback route),流程會稍有不同:SDK 走 implicit flow,provider 把 session 放在網址 hash 帶回來,supabase-js 會在頁面載入時自動偵測並解析,你不需要手動呼叫 exchangeCodeForSession。這種情況下,你只要在應用啟動時掛上 onAuthStateChange 監聽 SIGNED_IN 事件,就能在 session 就緒時更新 UI:

// 純前端 SPA:不需要 callback route,SDK 自動從 hash 解析 session
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN' && session) {
    // OAuth 繞回後,session 已就緒 → 更新畫面、導向 dashboard
    console.log('登入成功:', session.user.email)
  }
})

五、讀取 provider 帶回的個人資料

登入後,provider 提供的個人資料(名字、頭像、email)會被放進使用者的 user_metadata。在 Server Component 讀取:

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

export default async function DashboardPage() {
  const supabase = await createClient()

  // 永遠用 getUser() 向 Auth 伺服器驗證,不要用 getSession()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) redirect('/login')

  // provider 帶回的資料進 user_metadata(僅供顯示,不可用於授權判斷)
  const displayName = user.user_metadata?.full_name ?? user.user_metadata?.name
  const avatar = user.user_metadata?.avatar_url

  // 是用哪個 provider 登入的,記在 app_metadata(由系統維護,較可信)
  const provider = user.app_metadata?.provider // 例如 'github'

  return (
    <div>
      <img src={avatar} alt="avatar" width={48} height={48} />
      <p>歡迎,{displayName}(透過 {provider} 登入)</p>
    </div>
  )
}

若你需要 provider_token(拿去呼叫 provider 自己的 API,例如列出使用者的 GitHub repo),注意它不會存進資料庫,必須在登入完成當下、於 onAuthStateChange 事件中立即取出並自行保存:

supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN' && session) {
    // provider 的 access token(用來打 provider 自己的 API)
    const providerToken = session.provider_token
    // provider 的 refresh token(需要 access_type=offline 才會有)
    const providerRefreshToken = session.provider_refresh_token
    // 這兩者不會自動保存,需要時要自己存進 secure storage 或後端
  }
})

六、scopes 與多身份綁定觀念

前面在 Google 範例用了 scopes: 'openid email profile'。scopes(權限範圍)決定你向 provider 要求哪些權限:預設通常只拿基本的 email 與 profile,但若你的應用要進一步操作 provider 的資源——例如列出使用者的 GitHub repo、讀寫他的 Google Drive——就得在 scopes 明確列出對應範圍,使用者授權時也會看到你要求了哪些權限。範例:

// 要求讀取使用者的 GitHub repo 清單
await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
    scopes: 'read:user repo', // GitHub 的權限範圍
  },
})

拿到授權後,就能用前面提到的 session.provider_token 去呼叫 GitHub API。記住兩個原則:只要求你真正需要的 scope(要越多,使用者越可能猶豫不授權),以及 provider_token 不會自動保存,要在 onAuthStateChange 當下取出。

另外一個常見需求是同一使用者用多個 provider 登入。Supabase 的 auth.identities 資料表支援一個 auth.users 對應多筆身份(identity),也就是同一個帳號可以同時綁 Google 與 GitHub。當使用者已登入、想再綁一個 provider 時,用 linkIdentity:

// 已登入的使用者,額外綁定一個 GitHub 身份到同一帳號
const { data, error } = await supabase.auth.linkIdentity({
  provider: 'github',
})
// 同樣會走一次 OAuth 重定向 → callback,完成後兩個 provider 都指向同一使用者

要不要自動合併「用不同 provider、但 email 相同」的帳號,牽涉安全考量(見下方 email 已驗證的討論),Supabase Dashboard 有對應開關可控制此行為,請依你的信任模型決定。

常見錯誤與最佳實踐

坑一:把「Supabase 的 callback」和「你網站的 redirectTo」搞混。 在 Google Cloud/GitHub 的「Authorized redirect URIs」填成了 https://example.com/auth/callback,結果 provider 拒絕回呼、或整個流程斷在半路。正確做法:provider 那邊只填 Supabase 固定的 https://<project-ref>.supabase.co/auth/v1/callback;你自己網站的 redirectTo(example.com/auth/callback)是填在 signInWithOAuth 的 options,並加進 Supabase 的 Redirect URL Allowlist。兩個網址各有其位,別互換。

坑二:redirectTo 沒加進 Redirect URL Allowlist。 和上一篇一樣,這是最高頻的斷點——使用者授權完成、Supabase 卻不肯導回你的網站,因為那個網址不在白名單。正確做法:把每一個會用到的 callback 網址(生產、http://localhost:3000/**、Vercel preview…)都加進 Dashboard → Authentication → URL Configuration。

坑三:忘了寫 callback route,或以為 signInWithOAuth 就會登入。 只呼叫 signInWithOAuth,卻沒有一個 route 呼叫 exchangeCodeForSession,於是使用者繞完一圈回來、網址上有 code,卻沒人拿去換 session——結果永遠登不進去。正確做法:SSR 情境務必建立 /auth/callback Route Handler,在裡面 exchangeCodeForSession(code),這是 PKCE 流程收尾的必要一步。

坑四:把 client secret 外洩或寫死進版控。 client secret 等同你這個 OAuth app 的密碼,一旦洩漏,別人可冒用你的 app 身份。正確做法:secret 只貼在 Supabase Dashboard 或用環境變數(env(...))帶入 config.toml,絕不寫死在程式碼、.env 也要進 .gitignore。前端只會用到 anon key,不會、也不該碰到 client secret。

坑五:拿 user_metadata 當授權依據。 OAuth 回傳的 full_name、email、甚至你自己塞的欄位,全都落在 user_metadata——而 user_metadata 使用者可以自己用 updateUser() 改。若你用它判斷「是不是管理員」,等於把權限交到使用者手上。正確做法:授權判斷只依據 app_metadata(僅 service_role 的 Admin API 可改)或自建的 user_roles 資料表,搭配 RLS Policy 落實;user_metadata 只拿來顯示。

最佳實踐總結:

  • 分清兩個 callback:provider 那邊填 Supabase 的 .../auth/v1/callback,你網站的 redirectTo 填在 options 並進 Allowlist
  • callback route 不可少:SSR 情境務必用 exchangeCodeForSession(code) 收尾 PKCE 流程
  • secret 永不外洩:只放 Dashboard 或環境變數,不進版控、不進前端
  • 走 PKCE:用 @supabase/ssr 預設的 PKCE,別再用 implicit flow
  • user_metadata 只顯示不授權:權限靠 app_metadata 或 user_roles + RLS

小結

這是 Supabase 系列教學的第 021 篇。上一篇《Email/Password 驗證》我們用最經典的 email 密碼走完了註冊、確認、登入、密碼重設的完整循環,並在文末預告:其他登入方式只是換了入口、出口一致。這一篇就是兌現——OAuth 社群登入換掉的只是「驗證身份」那一段(改由 Google/GitHub 代勞),繞完一圈拿到的仍是同一組 session。

核心觀念濃縮成一句話:OAuth 社群登入的主線是「前端 signInWithOAuth({ provider }) 觸發跳轉 → 使用者在 provider 授權 → 帶著 code 導回你的 callback → callback 用 exchangeCodeForSession(code) 換回 session」;整段交換由 PKCE 保護,登入後 provider 的個人資料進 user_metadata(僅供顯示);而設定成敗的關鍵,在於分清『Supabase 的 callback URL』與『你網站的 redirectTo』,以及後者要進 Redirect URL Allowlist。

  • provider 設定:provider 端填 Supabase 的 .../auth/v1/callback,Supabase 端貼 client id/secret
  • 觸發登入:signInWithOAuth({ provider, options: { redirectTo, scopes } }),此時尚無 session
  • PKCE 交換:callback route 用 exchangeCodeForSession(code) 換回 session,是登入真正完成的一步
  • 讀 provider 資料:從 user_metadata 拿名字頭像顯示,provider_token 需在 onAuthStateChange 當下取出
  • 安全底線:client secret 不外洩、user_metadata 不可信任、授權靠 app_metadata 或 user_roles

把 OAuth 這條路走通後,你已經同時掌握了「有密碼」和「借用第三方身份」兩種登入。下一篇《Magic Link 與 OTP》,我們走向另一個極端——完全不要密碼:使用者只留一個 email,我們寄一條神奇連結或一組驗證碼,點一下或輸入六位數就登入。同樣一組 session,換一種最低摩擦的進門方式。下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →