第三方登入流程
第三方登入看起來像是一顆按鈕:
使用 Google 登入
使用 LINE 登入
使用 GitHub 登入但真正重要的不是按鈕,而是按下去之後系統做了什麼。
很多人會把第三方登入理解成:
拿到 Google token,所以使用者登入成功。這個說法太粗糙,也容易導致安全問題。
比較正確的理解是:
第三方 Provider 幫你驗證外部身份,
你的系統再根據這個外部身份建立或找到本地帳號,
最後建立自己的登入狀態。第三方登入不是把登入能力完全外包給 Google、LINE、GitHub,而是把「身份驗證的一部分」交給可信任 Provider,然後你的系統仍要處理帳號、權限、session、風險控制與稽核。
一、 一句話回答
如果面試官問:
第三方登入的流程是什麼?
可以先回答:
第三方登入通常會使用 OAuth 2.0 Authorization Code Flow,若 Provider 支援 OpenID Connect,還會取得 ID Token 來驗證使用者身份。流程是前端導向 Provider 登入頁,使用者授權後 Provider 帶著 code 回到 callback,後端用 code 換 token,驗證 state、ID Token、issuer、audience、expiration、nonce 等資訊,再用 provider + issuer + subject 或 provider user id 對應本地帳號。找到或建立本地 user 後,系統會建立自己的 session 或 token,而不是直接把第三方 token 當成本地登入狀態。
這題的核心不是「會不會串 Google Login」,而是你是否理解:
- OAuth / OIDC 各自負責什麼。
- 前端和後端的責任邊界。
- Provider token 和本地 session 的差異。
- 外部身份如何綁定本地帳號。
- email 衝突和帳號合併如何安全處理。
二、 第三方登入總流程
典型流程如下:
可以把它拆成四段:
| 階段 | 重點 |
|---|---|
| 發起登入 | 產生 state、nonce、PKCE,導向 Provider |
| Callback | Provider 帶 code 回來,後端驗證 state |
| 身份驗證 | 換 token,驗證 ID Token 或呼叫 Provider API |
| 本地登入 | 對應本地 user,建立本地 session |
三、 前端發起登入
前端通常只做一件事:
把使用者導向後端產生的第三方登入 URL。例如:
async function loginWithGoogle() {
const res = await fetch('/api/auth/google/login')
const { url } = await res.json()
window.location.href = url
}後端會產生類似這樣的 authorization URL:
https://accounts.google.com/o/oauth2/v2/auth
?client_id=...
&redirect_uri=https://example.com/api/auth/google/callback
&response_type=code
&scope=openid%20email%20profile
&state=...
&nonce=...
&code_challenge=...
&code_challenge_method=S256幾個重要參數:
| 參數 | 用途 |
|---|---|
| client_id | 你的應用在 Provider 註冊的識別 |
| redirect_uri | 登入完成後 Provider 導回的位置 |
| response_type=code | 使用 Authorization Code Flow |
| scope | 要求的授權範圍 |
| state | 防 CSRF 與登入流程混淆 |
| nonce | OIDC 用來防 ID Token replay |
| code_challenge | PKCE 用來保護 authorization code |
前端不應該自己組敏感流程,例如直接把 client_secret 放在瀏覽器。只要是 browser、mobile app、SPA 這類 public client,就不能把 secret 當秘密保存。
四、 Callback 與 code exchange
使用者在 Provider 完成登入後,Provider 會導回你的 callback:
GET /api/auth/google/callback?code=abc&state=xyz後端第一步不是換 token,而是先驗證 state:
app.get('/api/auth/google/callback', async (req, res) => {
const { code, state } = req.query
const savedState = req.cookies.oauth_state
if (!state || state !== savedState) {
throw new Error('Invalid OAuth state')
}
const tokens = await exchangeCodeForTokens({
code,
redirectUri: GOOGLE_REDIRECT_URI,
codeVerifier: req.cookies.pkce_verifier,
})
// 後續驗證身份、建立本地 session
})state 的用途不是裝飾品,它至少要防:
- CSRF。
- 使用者登入流程被換掉。
- callback 被攻擊者偽造。
- 多分頁登入流程互相污染。
如果使用 PKCE,後端或前端在發起登入時會產生:
code_verifier -> code_challenge換 token 時必須提交 code_verifier。Provider 會確認它和一開始的 code_challenge 對得上,避免 authorization code 被攔截後直接換 token。
五、 ID Token 與 UserInfo 驗證
如果 Provider 支援 OpenID Connect,例如 Google、LINE,登入流程通常會拿到 id_token。
ID Token 是給 Client 驗證使用者身份的 token,常見格式是 JWT。
後端不能只做:
const payload = decodeJwt(idToken)只 decode 不驗證是危險的。正確做法要驗:
| 驗證項目 | 說明 |
|---|---|
| signature | 確認 token 由可信 Provider 簽發 |
| issuer | 確認簽發者是預期 Provider |
| audience | 確認 token 是發給你的 client_id |
| expiration | 確認 token 沒過期 |
| nonce | 確認對應本次登入流程 |
| subject | 取得使用者在 issuer 底下的穩定識別 |
| email_verified | 如果要信任 email,必須確認是否已驗證 |
例如 OIDC payload 可能長這樣:
{
"iss": "https://accounts.google.com",
"sub": "110248495921238986420",
"aud": "your-google-client-id",
"exp": 1710000000,
"iat": 1709996400,
"nonce": "random-nonce",
"email": "user@example.com",
"email_verified": true,
"name": "Example User",
"picture": "https://example.com/avatar.png"
}最重要的是 iss + sub。
不要只用 email 當第三方登入的唯一身份 key,因為:
- 有些 Provider 不一定回傳 email。
- 使用者可能更換 email。
- GitHub 使用者可能隱藏 email。
- email 可能尚未驗證。
- 不同 Provider 可能回傳同一個 email,但不代表可以自動合併帳號。
比較安全的外部身份識別是:
provider + issuer + subject或在非 OIDC Provider 中使用:
provider + provider_user_id六、 本地帳號與 External Identity
第三方登入成功後,不代表你的系統可以不需要 user table。
通常會有兩張表:
users
external_identities概念如下:
type User = {
id: string
email: string | null
name: string | null
avatarUrl: string | null
createdAt: Date
}
type ExternalIdentity = {
id: string
userId: string
provider: 'google' | 'line' | 'github'
issuer: string | null
subject: string
email: string | null
emailVerified: boolean
profile: unknown
createdAt: Date
}登入時的查找流程:
async function handleExternalLogin(identity: ExternalIdentityPayload) {
const linkedIdentity = await db.externalIdentity.findUnique({
where: {
provider_issuer_subject: {
provider: identity.provider,
issuer: identity.issuer,
subject: identity.subject,
},
},
include: { user: true },
})
if (linkedIdentity) {
return linkedIdentity.user
}
return createOrLinkUser(identity)
}重點是:
外部身份先對 external_identities,
external_identities 再對 users。不要直接把 Provider 的 user id 當成本地 user id。你的系統應該保留自己的使用者模型,才能支援:
- 同一個使用者綁多個登入方式。
- 本地帳號和第三方帳號共存。
- MFA。
- 權限與角色。
- 組織與 tenant。
- 停權、刪除、稽核。
- 第三方 Provider 遷移。
七、 Google、LINE、GitHub 的差異
第三方登入的抽象流程類似,但每個 Provider 的身份資料來源不完全一樣。
Google Login
Google 是典型 OIDC Provider。
常見 scope:
openid email profile你通常會取得:
- ID Token。
iss。sub。email。email_verified。name。picture。
實務上要以 iss + sub 當穩定身份,不要只靠 Gmail 地址。
LINE Login
LINE Login 也支援 OpenID Connect。若 scope 包含 openid,可以取得 ID Token。
常見資料包含:
- LINE user id。
- display name。
- picture。
- email,視 scope 與設定而定。
LINE 的 email 不一定永遠都有,所以系統設計上不能假設第三方登入一定有 email。若你的產品必須要 email,登入後可以補一段「補填並驗證 email」流程。
GitHub Login
GitHub OAuth Apps 常見流程更偏 OAuth API 存取。
GitHub 使用者資料通常來自:
GET /user
GET /user/emails要注意:
/user的 email 可能是null。- 使用者可能把 email 設成 private。
- 如果要用 email,應查詢 verified email。
- GitHub user id 才是比較穩定的外部識別。
所以 GitHub 登入常見外部 identity 會像:
provider = github
subject = github_user_id
email = verified_primary_email 或 null八、 Email 衝突與帳號綁定
第三方登入最容易出事故的地方是 email 衝突。
假設資料庫已經有:
user A: alice@example.com,用帳密註冊後來有人用 Google 登入,也回傳:
email = alice@example.com
email_verified = true
sub = google-sub-123能不能直接把 Google identity 綁到 user A?
不能一概而論。
比較安全的策略是:
| 情境 | 建議 |
|---|---|
| 使用者已登入本地帳號,主動綁 Google | 可以綁定 |
| 使用者未登入,但 Google email verified 且本地帳號也已驗證 | 可要求再驗證本地密碼或 email OTP 後綁定 |
| email 未驗證 | 不自動合併 |
| Provider 不提供 email | 建立新帳號或要求補驗 email |
| 多個帳號同 email | 不自動判斷,進入人工或額外驗證流程 |
帳號綁定要保守,因為錯誤合併帳號會變成帳號接管。
常見安全做法:
- 已登入狀態下才能新增第三方登入方式。
- 綁定前要求 re-authentication。
- 綁定成功寄 email 通知。
- 解除綁定前確認還有其他登入方式。
- 對高風險綁定操作記錄 audit log。
九、 建立本地 Session
第三方身份驗證成功後,最後一步是建立你自己的登入狀態。
例如:
const session = await createSession({
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent'],
})
res.cookie('sid', session.id, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
})
res.redirect('/app')這裡的 sid 是你自己的 session id,不是 Google access token,也不是 LINE ID Token。
這樣設計有幾個好處:
- 你可以自己控制 session 有效期。
- 可以登出單一裝置或全部裝置。
- 可以撤銷 session。
- 可以做 remember me。
- 可以接 MFA。
- 可以做風險控管。
- 第三方 token 過期不一定影響本地 session 策略。
如果你的系統使用 JWT,也應該是簽發自己的 access token / refresh token,而不是把 Provider token 發給前端到處使用。
十、 第三方 Token 要不要存?
這要看產品是否需要呼叫 Provider API。
只做登入
如果只是使用 Google / LINE / GitHub 來登入,通常不需要長期保存 provider access token。
你只需要保存:
- provider。
- issuer。
- subject 或 provider user id。
- email 與驗證狀態。
- profile snapshot。
- linked time。
需要呼叫 Provider API
如果產品需要讀取 GitHub repo、Google Calendar、LINE profile 更新等,就可能需要保存 access token 或 refresh token。
這時要額外考慮:
- 加密保存 token。
- 最小 scope。
- token rotation。
- revoke provider access。
- 使用者解除授權後的同步。
- 背景任務失敗重試。
- provider API rate limit。
第三方登入和第三方 API 授權可以是同一套 OAuth 流程,但產品語意不一樣:
登入:你是誰?
授權:你允許我做什麼?十一、 Redirect 安全
很多登入流程會帶一個登入後返回頁:
/login?redirect=/checkout第三方登入時常會把這個資訊放進 state 或 server-side 暫存。
不要允許任意 URL:
/login?redirect=https://evil.example.com這會造成 open redirect,攻擊者可以借你的登入流程導向釣魚站。
比較安全的做法:
- 只允許相對路徑。
- 或只允許白名單 domain。
- 將 redirect target 存在 server-side。
- state 只放不可預測的 id,不直接暴露完整資訊。
例如:
function normalizeRedirect(input: string | undefined) {
if (!input || !input.startsWith('/')) {
return '/app'
}
if (input.startsWith('//')) {
return '/app'
}
return input
}十二、 常見錯誤
1. 直接信任 email
錯誤:
只要第三方回傳 email 一樣,就自動登入既有帳號。正確:
使用 provider + issuer + subject 找 external identity。
email 只能輔助判斷,且要確認 verified。2. 只 decode ID Token,不驗簽
錯誤:
const user = jwtDecode(idToken)正確:
驗 signature、iss、aud、exp、nonce,再信任 claims。3. 把 Provider access token 當本地 session
錯誤:
前端保存 Google access token,之後每次拿它呼叫自己的 API。正確:
Provider token 用於和 Provider 互動。
自己的 API 使用自己的 session 或 token。4. 忘記 state / nonce / PKCE
錯誤:
只要 callback 有 code 就換 token。正確:
驗 state,OIDC 驗 nonce,public client 使用 PKCE。5. callback endpoint 太寬鬆
錯誤:
任何 redirect_uri 都可用。正確:
Provider 後台設定固定 redirect_uri,後端換 token 時也使用完全一致的 redirect_uri。十三、 面試追問題庫
Q1:第三方登入成功後,可以直接用第三方 token 當 session 嗎?
不建議。
第三方 token 是 Provider 簽發的,語意是給 Provider 或特定 resource server 使用。你的系統應該在驗證外部身份後,建立自己的 local session 或自己的 access token,這樣才能控制有效期、登出、撤銷、權限、稽核與風險策略。
Q2:Google Login 和 OAuth Login 是同一件事嗎?
不完全是。
OAuth 2.0 本質是授權框架,解決的是第三方應用如何取得 API 存取授權。Google Login 常會使用 OpenID Connect,在 OAuth 2.0 上加入身份層,透過 ID Token 讓 Client 驗證使用者是誰。
Q3:為什麼不能只用 email 當第三方登入識別?
因為 email 不是所有 Provider 都保證提供,也可能未驗證、可變更、被隱藏,甚至不同 Provider 回傳同一 email 也不代表可以直接合併帳號。穩定身份應使用 issuer + subject 或 provider + provider_user_id。
Q4:第三方登入如何處理既有帳號?
可以分成兩種:
- 使用者已登入本地帳號後主動綁定第三方登入。
- 使用者未登入但第三方 email 和既有帳號相同,此時應要求額外驗證,例如本地密碼、email OTP 或其他 re-authentication。
不要只因 email 相同就自動合併。
Q5:GitHub 登入為什麼比較常遇到 email 問題?
因為 GitHub 使用者可以隱藏 email,/user API 回傳的 email 可能是 null。如果系統需要 email,應額外查 /user/emails,找 verified primary email;如果仍沒有,就要求使用者補填並驗證 email。
Q6:state 和 nonce 的差別是什麼?
state 主要用來保護 OAuth 流程,防 CSRF 和流程混淆。
nonce 是 OIDC 用來綁定登入請求與 ID Token,防止 ID Token replay。
兩者解決的問題不同,不應混用。
Q7:第三方登入和帳號綁定有什麼差別?
第三方登入是使用外部 Provider 驗證身份並登入你的系統。
帳號綁定是把某個外部身份加入既有本地帳號,讓未來可用該 Provider 登入同一個 user。
綁定是高風險操作,通常要使用者已登入,並做二次確認或 re-authentication。
十四、 實作題
題目一:設計第三方登入資料表
請設計資料表支援:
- 使用者可用帳密登入。
- 使用者可綁定 Google、LINE、GitHub。
- 一個 user 可綁多個 provider。
- 同一個 provider identity 只能綁一個 user。
- 可記錄 email 是否 verified。
參考方向:
create table users (
id uuid primary key,
email text,
email_verified boolean not null default false,
name text,
avatar_url text,
created_at timestamp not null
);
create table external_identities (
id uuid primary key,
user_id uuid not null references users(id),
provider text not null,
issuer text,
subject text not null,
email text,
email_verified boolean not null default false,
profile_json jsonb,
created_at timestamp not null,
unique (provider, issuer, subject)
);題目二:設計 Google 登入 callback
請寫出 callback 要做哪些事:
- 取得
code和state。 - 驗證
state。 - 用
code換 token。 - 驗證 ID Token。
- 用
iss + sub查 external identity。 - 找到或建立本地 user。
- 建立本地 session。
- 設定 HttpOnly Cookie。
- 導回前端。
題目三:處理 email 衝突
情境:
資料庫已有 user: alice@example.com
使用者用 GitHub 登入,GitHub verified email 也是 alice@example.com請設計安全流程。
好的回答應該提到:
- 不直接自動合併。
- 可以提示使用者登入既有帳號後綁定 GitHub。
- 或要求 email OTP / 密碼確認。
- 成功綁定後寄通知信。
- 記錄 audit log。
十五、 資深視角
1. Provider 是身份來源,不是你的權限系統
Google 告訴你:
這個 Google subject 通過 Google 驗證。但它不會告訴你:
- 這個人是不是你的會員。
- 這個人能不能看這個訂單。
- 這個人是不是 admin。
- 這個人屬於哪個 tenant。
- 這個人的 session 是否應該被撤銷。
這些仍是你的系統責任。
2. Account linking 是安全敏感功能
第三方登入最危險的錯誤常不是 token 驗證,而是帳號合併策略太寬。
只要你錯把攻擊者控制的外部 identity 綁到受害者 user,就會形成 account takeover。
因此帳號綁定要具備:
- 明確使用者意圖。
- 足夠身份確認。
- 可追蹤紀錄。
- 可撤銷能力。
- 解除綁定的保護。
3. 不同 Provider 要有同一個內部模型
Google、LINE、GitHub 回傳資料不同,但系統內部應收斂成同一個模型:
ExternalIdentity這樣後續接 Apple、Microsoft、Facebook、企業 OIDC、SAML 時,不會讓 user table 變成各種欄位的大雜燴。
4. 登入流程要能被觀測
成熟系統會記錄:
- login_started。
- provider_callback_received。
- state_invalid。
- token_exchange_failed。
- identity_verified。
- account_linked。
- account_created。
- session_created。
- login_failed。
這不只是除錯,也是安全稽核與風險分析的基礎。
總結
| 問題 | 回答重點 |
|---|---|
| 第三方登入是什麼 | 使用外部 Provider 驗證身份,再建立本地登入狀態 |
| 是否直接用 Provider token | 不應該,應建立自己的 session/token |
| OIDC Provider 怎麼驗身份 | 驗 ID Token 的簽章、iss、aud、exp、nonce、sub |
| 外部身份怎麼對應 user | 使用 provider + issuer + subject |
| email 能不能當唯一 key | 不建議,只能輔助且要確認 verified |
| GitHub 有什麼特別 | email 可能為 null,要查 verified email 或補驗 |
| 帳號綁定最大風險 | 錯誤合併造成 account takeover |
| 登入後做什麼 | 建立本地 session、設定 Cookie、回到前端 |
一句完整的面試回答可以是:
第三方登入不是直接拿 Google、LINE、GitHub 的 token 當作本地 session,而是透過 OAuth 2.0 或 OpenID Connect 完成外部身份驗證。前端導向 Provider,Provider 登入後帶 authorization code 回 callback,後端驗證 state,使用 code 換 token;如果是 OIDC,還要驗證 ID Token 的簽章、issuer、audience、expiration、nonce 和 subject。系統應以 provider + issuer + subject 或 provider user id 查找 external identity,再對應本地 user,必要時處理 email verified、帳號建立、帳號綁定與 email 衝突。最後由自己的系統建立 session 或 token,並負責權限、登出、撤銷、MFA、稽核與風險控制。