From 8439a1fcd5ef82d20521df6616b16b17f63f2c96 Mon Sep 17 00:00:00 2001 From: YJack0000 Date: Fri, 28 Aug 2026 18:54:23 +0800 Subject: [PATCH] =?UTF-8?q?[fix]=20SSO=20=E7=99=BB=E5=85=A5=E6=92=9E?= =?UTF-8?q?=E6=97=A2=E6=9C=89=20Google=20=E5=B8=B3=E8=99=9F=E5=9B=9E=20err?= =?UTF-8?q?or=3DUNKNOWN=EF=BC=9A=E9=96=8B=20domainVerification=E3=80=81?= =?UTF-8?q?=E8=A3=9C=20domain=5Fverified=20=E6=AC=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/0021_sso_domain_verified.sql | 19 +++++++++++++++++++ scripts/register-sso-provider.ts | 4 ++++ src/db/auth-schema.ts | 14 ++++++++++---- src/lib/auth.ts | 8 +++++++- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 migrations/0021_sso_domain_verified.sql diff --git a/migrations/0021_sso_domain_verified.sql b/migrations/0021_sso_domain_verified.sql new file mode 100644 index 0000000..0c23684 --- /dev/null +++ b/migrations/0021_sso_domain_verified.sql @@ -0,0 +1,19 @@ +-- 0021: sso_provider.domain_verified(@better-auth/sso 的 domainVerification)。 +-- +-- 為什麼需要:OIDC callback 判斷「這個 provider 可不可以信任」的唯一條件是 +-- provider.domainVerified === true 且登入者 email 的網域與 provider.domain 相符 +-- (@better-auth/sso dist/index.mjs 的 isTrustedProvider)。不信任的 provider +-- 不能把 SSO 登入 link 到既有的同 email user —— 已經用 Google 登入過的員工改走 +-- SSO 會直接被 ?error=UNKNOWN 踢回首頁(2026-08-28 上線當天發生)。 +-- +-- 我們不用 plugin 的 DNS TXT 驗證流程:HTTP 註冊端點整條封死(見 0020 的註解), +-- 註冊一律走 scripts/register-sso-provider.ts,該腳本從本 migration 起直接寫 +-- domain_verified = true —— 管理者用腳本註冊即視為已驗證。 +-- +-- Forward-only, additive. Run AFTER 0020_sso_provider.sql. + +ALTER TABLE sso_provider + ADD COLUMN domain_verified boolean NOT NULL DEFAULT false; + +-- 既有的 provider 都是腳本註冊的(HTTP 路徑從未開放),一併視為已驗證。 +UPDATE sso_provider SET domain_verified = true; diff --git a/scripts/register-sso-provider.ts b/scripts/register-sso-provider.ts index 9dae1d8..31019f0 100644 --- a/scripts/register-sso-provider.ts +++ b/scripts/register-sso-provider.ts @@ -171,6 +171,10 @@ await ctx.adapter.create({ samlConfig: null, userId, organizationId, + // 管理者用腳本註冊即視為已驗證網域(我們不用 plugin 的 DNS TXT 流程)。 + // 這是 OIDC callback 肯把 SSO 登入 link 到既有同 email user 的前提, + // 見 migrations/0021_sso_domain_verified.sql。 + domainVerified: true, }, }); diff --git a/src/db/auth-schema.ts b/src/db/auth-schema.ts index b33032a..81a6f2b 100644 --- a/src/db/auth-schema.ts +++ b/src/db/auth-schema.ts @@ -145,10 +145,12 @@ export const oauthConsent = pgTable("oauth_consent", { // providerId / organizationId / domain。plugin 沒有為這個 model 宣告 // createdAt / updatedAt,所以這裡也沒有。 // -// `domainVerified` 只在 plugin 開了 `domainVerification` 時才會出現在 schema 裡, -// 本專案沒開,所以不放。adapter 是照 schema 的欄位逐一取值(@better-auth/core -// 的 adapter factory:`for (const field in fields)`),schema 裡沒有的輸入欄位 -// 會被忽略而不是報錯,所以少這一欄不會炸。 +// `domainVerified` 隨 plugin 的 `domainVerification` 選項存在(auth.ts 有開)。 +// 它是 OIDC callback 信任判斷的唯一依據:provider.domainVerified === true 且 +// email 網域與 provider.domain 相符,才允許把這次 SSO 登入 link 到既有的同 +// email user(better-auth link-account 的 isTrustedProvider)。少了它,已用 +// Google 登入過的人走 SSO 會被拒絕連結,整個 callback 以 ?error=UNKNOWN 收場 +// —— 2026-08-28 上線當天就踩到。 // // export 名稱必須正好是 `ssoProvider`:drizzle adapter 用 model 名稱去 schema // 物件上取表。DB 表名照本專案慣例用 snake_case,見 migrations/0020_sso_provider.sql。 @@ -164,4 +166,8 @@ export const ssoProvider = pgTable("sso_provider", { organizationId: text("organization_id"), // home realm discovery 的鍵:登入時拿 email 的網域來比對這一欄。 domain: text("domain").notNull(), + // 我們沒有走 plugin 的 DNS TXT 驗證流程(HTTP 註冊整條封死了);這欄由 + // scripts/register-sso-provider.ts 在註冊時直接寫 true —— 管理者用腳本註冊 + // 就是驗證。migration: 0021_sso_domain_verified.sql。 + domainVerified: boolean("domain_verified").notNull().default(false), }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ebff8d1..36b6b23 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -186,7 +186,13 @@ export const auth = betterAuth({ // // 刻意不設 organizationProvisioning:第一次 SSO 登入會自動建 user // (implicit signup,這是我們要的),但要進哪個組織仍走既有的邀請流程。 - sso({ providersLimit: 0 }), + // domainVerification 是 OIDC callback 的信任開關:開了之後 plugin 會讀 + // sso_provider.domain_verified,「已驗證網域的 provider + email 網域相符」 + // 才算 trusted provider,better-auth 才肯把 SSO 登入 link 到既有的同 email + // user(例如先用 Google 登入過的員工)。不開的話 link 一律被拒,走 SSO 的 + // 老用戶會拿到 ?error=UNKNOWN(2026-08-28 實際發生過)。我們不用它附帶的 + // DNS TXT 驗證流程 —— 註冊走腳本、domain_verified 由腳本直接寫 true。 + sso({ providersLimit: 0, domainVerification: { enabled: true } }), // OAuth 2.0 / OIDC provider for MCP clients. Adds /api/auth/mcp/* endpoints // (authorize, token, register, get-session) + OAuth discovery. Unauthenticated // authorize requests are sent to loginPage, which redirects back after sign-in.