← 回文章列表

2023年10月24日

OpenID Connect 登入流程拆解:Authorization Code + PKCE、id_token 與 access_token 的差別

OpenID ConnectOAuth 2.0JWT身分驗證ASP.NET Core資訊安全

OAuth 2.0 和 OpenID Connect 差在哪

這兩個名詞常被混用,但它們解決的是不同的問題:

  • OAuth 2.0 是授權(Authorization)協定:讓一個應用程式取得「代表使用者存取某個資源」的權限,拿到的是 access_token。它本身沒有定義「這個使用者是誰」
  • OpenID Connect(OIDC) 建立在 OAuth 2.0 之上,補上了身分驗證(Authentication):除了 access_token,還多給一張 id_token,裡面寫著「這次登入的使用者是誰」,並且定義了標準的使用者資訊欄位、探索端點(Discovery)、公鑰端點(JWKS)

簡單說:要知道「你是誰」用 OIDC,要取得「你能做什麼」用 OAuth 2.0。企業內部系統串接 SSO,需要的通常是兩者都有的 OIDC。

完整流程:Authorization Code + PKCE

瀏覽器上的單頁應用(SPA)目前建議使用的流程是 Authorization Code Flow 搭配 PKCE:

OpenID Connect Authorization Code 加 PKCE 流程:瀏覽器先產生隨機的 code_verifier 並算出 code_challenge;① 導向 OpenID Provider 的 authorize 端點並帶上 scope=openid、code_challenge 與 state;② 使用者輸入公司帳號密碼或 MFA;③ OpenID Provider 帶著授權碼導回 redirect_uri;④ 瀏覽器以授權碼加 code_verifier 呼叫 token 端點,OpenID Provider 驗證 SHA256(verifier) 等於 challenge;⑤ 回傳 id_token 與 access_token;⑥ 瀏覽器以 Bearer access_token 呼叫後端 API 或建立 SignalR 連線;⑦ 後端啟動時下載 JWKS 公鑰,在本機驗證簽章、iss、aud、exp,並取出 sub 作為使用者唯一 ID

① 導向授權端點

使用者按下登入,前端把瀏覽器導向 OpenID Provider(公司的帳號中心)的 /authorize:

text
https://sso.example.com/authorize
  ?response_type=code
  &client_id=nexus-gpt
  &redirect_uri=https://gpt.example.com/callback
  &scope=openid profile email
  &state=8f2c...           ← 防 CSRF:回來時要比對
  &code_challenge=E9Mel...  ← PKCE
  &code_challenge_method=S256

scope 裡的 openid 是關鍵,代表這是一次 OIDC 登入,要求回傳 id_token。

② 使用者在 OpenID Provider 登入

帳號密碼、MFA 都在帳號中心的頁面上輸入,應用程式從頭到尾都碰不到使用者的密碼。這是 SSO 最大的價值:只有一個地方需要保護密碼。

③ 帶著授權碼導回

登入成功後,帳號中心把瀏覽器導回 redirect_uri,網址上帶著一次性的**授權碼(code)**和原本的 state。前端要先比對 state 和自己發出去的是否相同,不同就代表這不是自己發起的登入,直接拒絕。

④ 用授權碼換 Token

前端拿授權碼呼叫帳號中心的 /token 端點換取 Token。授權碼出現在網址上,可能被瀏覽器紀錄、外掛、或惡意的 redirect 攔截;PKCE 就是為了讓「攔截到授權碼的人也換不到 Token」:

  • 在步驟 ① 之前,前端先產生一個隨機字串 code_verifier,只存在自己手上
  • 步驟 ① 送出的是它的雜湊值 code_challenge = BASE64URL(SHA256(code_verifier))
  • 步驟 ④ 換 Token 時,要附上原始的 code_verifier,帳號中心算一次雜湊,和 ① 收到的 challenge 比對

攔截者只拿得到授權碼和雜湊值,拿不到原始的 verifier,所以換不到 Token。

⑤ 拿到 id_token 與 access_token

兩者通常都是 JWT(JSON Web Token),由帳號中心用私鑰簽章。

⑥ 帶 access_token 呼叫 API

之後前端呼叫後端 API 時,在 Authorization: Bearer <access_token> 標頭帶上 access_token。

⑦ 後端驗證 Token

後端不需要每次都問帳號中心「這張 Token 是不是真的」。它在啟動時從帳號中心的 Discovery 端點(/.well-known/openid-configuration)找到 JWKS(公鑰),之後每個請求都在本機用公鑰驗證簽章即可。

id_token 和 access_token:用途不同,不要混用

id_tokenaccess_token
給誰看前端應用程式後端 API(資源伺服器)
用途證明「誰登入了」,前端拿來顯示使用者名稱、頭像證明「有權限呼叫這個 API」
aud(對象)前端應用程式的 client_id後端 API 的識別名稱
該送到 API 嗎不該該

最常見的錯誤是把 id_token 送給後端 API 當作通行證。id_token 的 aud 是前端應用程式,後端如果有正確驗證 aud,應該要拒絕它;如果沒驗證,就等於任何應用程式拿到的 id_token 都能拿來呼叫你的 API。

後端該驗證什麼

ASP.NET Core 中,設定好 Authority 後,JWT Bearer 驗證會自動下載公鑰並檢查大部分項目:

C#
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://sso.example.com";   // 由此找到 Discovery 與 JWKS
        options.Audience  = "nexus-gpt-api";               // 只接受發給這個 API 的 Token
        options.MapInboundClaims = false;                  // 保留原始的 claim 名稱(sub、email…)
    });

驗證項目包括:

  • 簽章:確定 Token 是帳號中心簽發、內容沒有被竄改
  • iss(發行者):是我們信任的帳號中心
  • aud(對象):這張 Token 是發給這個 API 的
  • exp(到期時間):還沒過期

用 sub 當使用者 ID,不要用 email

Token 裡有好幾個看起來都能代表使用者的欄位,應該用哪一個存進資料庫,當作資料的擁有者?

  • sub(Subject):OIDC 規範保證它在同一個 OpenID Provider 內唯一且永久不變
  • email、preferred_username:使用者改名、換 email、帳號被回收再分配時都可能改變

所以資料表上的「擁有者」欄位應該存 sub:

C#
var userId = User.FindFirstValue("sub");
var rooms  = await _db.ChatRooms.Where(r => r.OwnerId == userId).ToListAsync();

SignalR 連線怎麼帶 Token

WebSocket 在瀏覽器中無法自訂標頭,SignalR 的做法是把 Token 放在查詢字串 access_token 上。後端要額外告訴 JWT 驗證從哪裡讀:

C#
options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var token = context.Request.Query["access_token"];
        if (!string.IsNullOrEmpty(token) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
        {
            context.Token = token;
        }
        return Task.CompletedTask;
    }
};

驗證通過後,Hub 裡可以用 Context.UserIdentifier 取得目前的使用者(預設取自 NameIdentifier claim,關閉了 claim 對應時,可以實作 IUserIdProvider 改成讀 sub)。因為查詢字串可能出現在存取日誌裡,記得在反向代理或日誌設定中遮蔽它。

使用情境:內部 GPT 平台串接公司 SSO

公司內部的 GPT 平台一開始沒有登入,聊天紀錄只能跟著瀏覽器走。串上公司的 OpenID Connect 之後:

  • 同仁直接用公司帳號登入,帳號停用後自然就無法使用,不需要另外維護帳號
  • 聊天室以 sub 作為擁有者存放,換電腦、換瀏覽器,登入後都是自己的聊天室與完整對話
  • 所有查詢、修改、刪除都以 Token 中的 sub 過濾,無法透過改網址看到別人的對話
  • 每段對話的 Token 用量都能歸戶到使用者,掌握各部門的實際用量

一句話總結

OIDC 是在 OAuth 2.0 之上加了「你是誰」的 id_token;瀏覽器應用使用 Authorization Code 加 PKCE,讓密碼只在帳號中心輸入、授權碼被攔截也換不到 Token;id_token 給前端、access_token 給後端,後端用公鑰驗證簽章、iss、aud、exp,並以永久不變的 sub 作為使用者在系統中的身分。