跳至主要內容
Skip to content

OpenID Connect 與 OAuth 的差異

OAuth 2.0 常被拿來做「第三方登入」,但嚴格來說,OAuth 2.0 本身是授權框架,不是身份驗證協議。真正補上登入身份層的是 OpenID Connect,也就是 OIDC。

一句話回答:OAuth 2.0 解決的是授權:Client 如何取得 Access Token 去存取 Resource Server;OpenID Connect 則是在 OAuth 2.0 之上加入身份驗證層,透過 openid scope、ID Token、UserInfo Endpoint 與標準 Claims,讓 Client 可以驗證 End-User 的身份。OAuth 的 Access Token 是給 API 用的,OIDC 的 ID Token 是給 Client 驗證登入身份用的,兩者不能混用。


一、 必考觀念

1. OAuth 授權,OIDC 登入

協議解決問題主要 token
OAuth 2.0Client 是否被授權存取 APIAccess Token
OpenID ConnectClient 如何知道使用者身份ID Token

OAuth 2.0 問:

text
這個 Client 能不能代表使用者存取某個 API?

OIDC 問:

text
這個登入使用者是誰?

這是最常見面試陷阱。

延伸讀:OAuth 2.0 解決什麼問題

2. OIDC 是 OAuth 2.0 的身份層

OpenID Connect Core 1.0 規格定義了:OIDC 是建立在 OAuth 2.0 之上的 authentication layer,並使用 Claims 傳遞 End-User 資訊。

OIDC 增加:

  • openid scope。
  • ID Token。
  • UserInfo Endpoint。
  • Standard Claims。
  • Discovery。
  • JWKS。
  • nonce。

3. 第三方登入通常是 OIDC

當你看到:

text
使用 Google 登入
使用 Microsoft 登入
使用 Okta 登入

背後通常是 OIDC,而不是純 OAuth。

原因是 Client 不只需要 access token,它還需要可信的使用者身份資訊。


二、 ID Token 是什麼?

1. ID Token 是給 Client 的身份聲明

ID Token 通常是 JWT,內容是 Authorization Server 對使用者身份的聲明。

範例:

json
{
  "iss": "https://accounts.example.com",
  "sub": "248289761001",
  "aud": "client_123",
  "exp": 1716533600,
  "iat": 1716530000,
  "nonce": "n-0S6_WzA2Mj",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Hirimu"
}

ID Token 的接收方是 Client。

Access Token 的接收方通常是 Resource Server。

2. ID Token 不是拿來呼叫 API

不要這樣:

http
Authorization: Bearer id_token

呼叫 API 應使用 access token。

ID Token 用途是:

text
Client 驗證使用者身份
建立本地登入狀態
綁定本地帳號

3. ID Token Payload 可讀,不等於可亂信

ID Token 常是 JWT,但 Client 必須驗證:

  • signature。
  • issuer。
  • audience。
  • expiration。
  • issued at。
  • nonce,若有使用。
  • authorized party,視情況。
  • key id / JWKS。

只 decode ID Token 是不夠的。

延伸讀:JWT 是什麼


三、 Access Token、ID Token、Refresh Token

1. 三者用途不同

Token給誰用用途
Access TokenResource Server呼叫 API
ID TokenClient驗證使用者身份
Refresh TokenClient / Auth Server換新的 token

不要混用。

2. Access Token 不一定能被 Client 解讀

有些 provider 的 access token 是 JWT。

有些是 opaque token。

即使是 JWT,Client 也不應假設 access token 的 claims 可作為登入身份。

因為:

  • audience 可能是 API,不是 Client。
  • claims 格式由 Resource Server 使用。
  • access token 可能不包含身份資訊。
  • access token 的語意是授權,不是登入身份。

3. ID Token 不代表本地授權完成

ID Token 可以告訴你:

text
外部 provider 認證了這個 subject。

但你的系統仍要決定:

  • 是否建立本地 user。
  • 是否允許登入。
  • 是否綁定既有帳號。
  • 是否需要 email verified。
  • 是否需要 MFA。
  • 本地角色與權限。

OIDC 解決身份資訊,不解決你系統內所有授權問題。


四、 OIDC Flow

1. Authorization Code Flow + OIDC

OIDC 通常會在 OAuth authorization request 加上:

text
scope=openid profile email

只要有 openid scope,就表示這是 OIDC request。

2. UserInfo Endpoint

Client 可以用 access token 呼叫 UserInfo Endpoint:

http
GET /userinfo
Authorization: Bearer access_token

得到:

json
{
  "sub": "248289761001",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Hirimu"
}

注意:

  • UserInfo 回來的 sub 應和 ID Token 的 sub 一致。
  • UserInfo 資料仍要依 provider 信任模型處理。

3. Discovery 與 JWKS

OIDC Provider 通常提供 discovery document:

text
/.well-known/openid-configuration

裡面包含:

  • authorization_endpoint。
  • token_endpoint。
  • userinfo_endpoint。
  • jwks_uri。
  • issuer。
  • supported scopes。
  • supported signing algorithms。

Client 可用 jwks_uri 取得 public keys 驗證 ID Token。


五、 Claims 與 Subject

1. sub 是使用者在 Provider 的穩定識別

OIDC 裡最重要的使用者識別通常是:

text
iss + sub

不要只用 email 當唯一身份。

原因:

  • email 可能變更。
  • email 可能未驗證。
  • 不同 provider 可能有相同 email。
  • 企業租戶可能重用 email。

本地帳號綁定應存:

typescript
type ExternalIdentity = {
  provider: 'google' | 'microsoft' | 'line'
  issuer: string
  subject: string
  userId: string
}

2. Email 不一定可信

OIDC 可能提供:

json
{
  "email": "user@example.com",
  "email_verified": true
}

要注意:

  • 是否有 email_verified
  • Provider 是否可信。
  • email 是否已被本地帳號使用。
  • 是否允許自動綁定。

對高風險系統,不應只靠 email 相同就自動合併帳號。

3. Standard Claims

OIDC 定義了許多 standard claims,例如:

  • name。
  • given_name。
  • family_name。
  • email。
  • email_verified。
  • picture。
  • locale。

這些通常用於 profile,不應直接當作權限。


六、 Nonce

1. Nonce 解決什麼?

nonce 用來把 authentication request 和 ID Token 綁在一起,防止 replay。

Client 發起:

text
nonce=random-value

ID Token 回來後包含:

json
{
  "nonce": "random-value"
}

Client 驗證:

text
idToken.nonce === storedNonce

OpenID Connect Core 規格要求:如果 Authentication Request 包含 nonce,Authorization Server 必須在 ID Token 中包含相同 nonce;Client 也必須驗證。

2. Nonce、State、PKCE 差異

機制解決問題
stateOAuth flow CSRF / client request binding
nonceID Token replay / authentication response binding
PKCEauthorization code interception

三者不是互相替代。

3. Authorization Code Flow 一定要 nonce 嗎?

不同 profile 與 provider 實作可能有細節差異。

保守做法:

  • 使用 state。
  • 使用 PKCE。
  • 若發送 nonce,就驗證 ID Token nonce。
  • 對 hybrid / implicit flow 更要注意 nonce。

現代 SPA / Web 最推薦仍是 Authorization Code + PKCE。


七、 驗證 ID Token

1. 驗證步驟

Client 應驗證:

text
取得 provider metadata
-> 取得 JWKS
-> 找到 kid 對應 public key
-> 驗證 signature
-> 驗證 iss
-> 驗證 aud
-> 驗證 exp / iat
-> 驗證 nonce,若使用
-> 驗證 sub 存在

簡化程式:

typescript
const claims = await oidcVerifier.verifyIdToken(idToken, {
  issuer: 'https://accounts.example.com',
  audience: 'client_123',
  nonce: storedNonce,
})

2. Audience 必須是你的 client_id

ID Token 的 aud 應該包含你的 client id:

json
{
  "aud": "client_123"
}

如果 aud 是別人的 client id,你不能接受。

3. Issuer 必須匹配

要驗:

text
iss === expected issuer

不要只看 token 是某個 key 簽的。Issuer 是信任邊界。

4. 不要自己手刻驗證細節

OIDC/JWT 驗證細節多,建議使用成熟 library。

但使用 library 也要正確設定:

  • issuer。
  • audience。
  • allowed algorithms。
  • clock tolerance。
  • nonce。
  • JWKS cache。

八、 第三方登入後的本地帳號

1. 本地仍要建立 user

typescript
type User = {
  id: string
  email?: string
  displayName?: string
  status: 'active' | 'disabled'
}

type ExternalIdentity = {
  id: string
  userId: string
  provider: string
  issuer: string
  subject: string
  email?: string
  emailVerified?: boolean
}

流程:

text
驗證 ID Token
-> 取得 iss + sub
-> 查 external identity
-> 找到就登入本地 user
-> 找不到就建立或進入綁定流程

2. 自動綁定要小心

危險做法:

text
如果 Google email == 本地 email,就直接綁定。

要考慮:

  • 本地 email 是否已驗證。
  • 外部 email 是否 verified。
  • 是否同一租戶。
  • 是否需要使用者先登入本地帳號。
  • 是否有帳號接管風險。

帳號綁定會在後面獨立成篇。

3. 第三方登入後建立本地 session

OIDC 驗證成功後,通常建立你的系統 session:

text
OIDC login success
-> find or create local user
-> create local session / token
-> set local cookie

後續你的系統不應每次都依賴外部 ID Token 判斷本地授權。


九、 常見錯誤

1. 拿 OAuth access token 當登入身份

錯誤:

text
拿到 access token
-> decode
-> 當作 user login

Access token 是給 Resource Server 的,不一定是給 Client 的身份資料。

2. 不驗 ID Token signature

只 decode:

typescript
decode(idToken)

不安全。

3. 不驗 aud / iss

這可能接受別的 client 或別的 issuer 的 token。

4. 只用 email 當外部身份主鍵

應使用:

text
issuer + subject

Email 可作為聯絡方式或輔助資料,不應是唯一外部身份主鍵。

5. 把 ID Token 長期當 session

ID Token 是外部身份聲明。

你的系統應建立自己的 session,管理自己的登出、權限、稽核與風險狀態。


十、 追問題庫

Q1:OIDC 和 OAuth 差在哪?

OAuth 2.0 是授權框架,用 Access Token 讓 Client 存取 Resource Server;OIDC 是 OAuth 2.0 上的身份層,用 ID Token 和 UserInfo 讓 Client 驗證使用者身份。

Q2:ID Token 是什麼?

ID Token 是 OpenID Provider 給 Client 的身份聲明,通常是 JWT,包含 iss、sub、aud、exp、iat、nonce 以及使用者 claims。

Q3:Access Token 和 ID Token 差在哪?

Access Token 給 Resource Server,用來呼叫 API;ID Token 給 Client,用來驗證使用者身份。ID Token 不應拿去呼叫 API。

Q4:為什麼不能只用 email 當使用者識別?

Email 可能變更、未驗證、不同 provider 可能重複。OIDC 應使用 issuer + subject 作為外部身份穩定識別。

Q5:nonce 有什麼用?

Nonce 用來把 authentication request 和 ID Token 綁定,防止 ID Token replay。若請求帶 nonce,回來的 ID Token 要包含相同 nonce,Client 必須驗證。

Q6:UserInfo Endpoint 有什麼用?

Client 可以用 access token 呼叫 UserInfo Endpoint 取得使用者 profile claims,例如 email、name、picture。UserInfo 的 sub 應與 ID Token 的 sub 一致。


十一、 實作題

題目:設計 OIDC Login Callback

需求:

  • 使用 Authorization Code + PKCE。
  • 驗證 state。
  • 交換 token。
  • 驗證 ID Token。
  • 建立或找到本地 user。
  • 建立本地 session。
typescript
async function handleOidcCallback(query: CallbackQuery) {
  const saved = oidcStateStore.get(query.state)

  if (!saved) {
    throw new InvalidStateError()
  }

  const tokenSet = await oidcClient.exchangeCode({
    code: query.code,
    redirectUri: 'https://app.example.com/auth/callback',
    codeVerifier: saved.codeVerifier,
  })

  const claims = await oidcClient.verifyIdToken(tokenSet.id_token, {
    issuer: 'https://accounts.example.com',
    audience: 'client_123',
    nonce: saved.nonce,
  })

  const externalIdentity = {
    provider: 'example',
    issuer: claims.iss,
    subject: claims.sub,
  }

  const user = await userService.findOrCreateByExternalIdentity({
    externalIdentity,
    email: claims.email,
    emailVerified: claims.email_verified,
    displayName: claims.name,
  })

  const session = await sessionService.create({
    userId: user.id,
    loginMethod: 'oidc',
  })

  return {
    session,
    user,
  }
}

重點:

  • state 必須驗。
  • code_verifier 必須用。
  • ID Token 必須驗簽與 claims。
  • 本地 user / session 仍由你的系統建立。

十二、 資深視角

1. OIDC 是身份交換,不是權限外包

OIDC provider 可以告訴你:

text
這個外部 subject 已被 provider 驗證。

但你的系統仍要決定:

  • 是否允許登入。
  • 是否需要綁定。
  • 是否需要本地 MFA。
  • 是否有本地權限。
  • 是否在正確 tenant。

2. Trust boundary 很重要

你信任哪個 issuer?

text
https://accounts.google.com
https://login.microsoftonline.com/{tenant}
https://your-company.okta.com

不同 issuer 的 subject 不能混在一起。

3. 企業登入要處理 tenant

企業 OIDC 常有:

  • organization。
  • tenant id。
  • domain restriction。
  • group claims。
  • SCIM provisioning。
  • IdP initiated login。

這已經接近 SSO 系統設計,會在後面的 SSO / SAML / OIDC 文章展開。


總結

問題回答重點
OAuth授權 API 存取
OIDC在 OAuth 2.0 上加入身份登入層
ID Token給 Client 驗證使用者身份
Access Token給 Resource Server 呼叫 API
UserInfo取得使用者 claims
subject使用者在 issuer 下的穩定識別
nonce防 ID Token replay
本地帳號OIDC 成功後仍要建立本地 user/session

一句完整的面試回答可以是:

OAuth 2.0 和 OIDC 的差異在於 OAuth 解決授權,OIDC 解決登入身份。OAuth 2.0 讓 Client 透過 access token 存取 Resource Server 上的 API;OpenID Connect 在 OAuth 2.0 之上加入 openid scope、ID Token、UserInfo Endpoint 和標準 claims,讓 Client 可以驗證 End-User 是誰。Access token 是給 Resource Server 的,不應拿來當登入身份;ID Token 是給 Client 的身份聲明,通常是 JWT,必須驗簽章、issuer、audience、expiration、nonce 和 subject。使用者的外部身份應以 issuer + subject 綁定本地 user,而不是只用 email。OIDC 登入成功後,系統仍要建立自己的 local session,並做本地權限、租戶、稽核與風險控制。

延伸閱讀