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 ID | provider(Google Cloud/GitHub)建立 OAuth app 後產生 | Supabase Dashboard → Authentication → Providers |
| Client Secret | 同上(機密,等同密碼) | 同上(Supabase 保管,勿外流) |
| Callback URL | Supabase 固定提供: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 URL | provider 授權後回呼的網址(Supabase 固定的 .../auth/v1/callback) |
redirectTo | Supabase 處理完後,導回你自己網站的網址 |
code | 授權碼,provider 回傳、用來換 session 的一次性憑證 |
exchangeCodeForSession | 在 callback route 用 code 換回 session |
scopes | 向 provider 要求的權限範圍(如讀 email、讀 repo) |
provider_token | provider 發的 access token,可拿去呼叫 provider 自己的 API |
實作範例
以下用 supabase-js(TypeScript)+ Next.js App Router 情境示範,這是最常見、也最需要 callback route 的組合。我們依「設定 → 觸發 → 交換 → 讀資料」的順序逐段實作。
一、Dashboard/provider 設定要點
程式碼之前,先把設定做對,否則怎麼寫都登不進去。以 GitHub 為例(Google 概念相同,只是在 Google Cloud Console 操作):
- 到 GitHub → Settings → Developer settings → OAuth Apps → New OAuth App。
- Authorization callback URL 填 Supabase 的固定 callback:
https://<project-ref>.supabase.co/auth/v1/callback。 - 建立後拿到 Client ID,並產生一組 Client Secret。
- 回 Supabase Dashboard → Authentication → Providers → GitHub → 貼入 Client ID/Secret → Enable → Save。
- 到 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,換一種最低摩擦的進門方式。下一篇見。