June 09, 2026

Backend | SSO Introduction with OIDC and NextAuth

這篇文章會從 SSO(Single Sign-On,單一登入)的基本概念開始介紹,接著說明 JWT 的結構,然後進入 OIDC(OpenID Connect),最後再解釋 NextAuth 在整個流程中的角色。

本文的目標是依照協定堆疊(protocol stack)的層次,由上而下逐步說明,讓讀者能夠清楚理解各個技術之間的關係,而不是將概念與實作細節混在一起討論。

1. SSO Introduction

SSO 的重點是把登入集中到同一個 IdP,讓多個應用共用同一組驗證結果,而不是讓每個系統自己各做一套登入流程。

SSO 的核心不是「一次登入就永遠不用登入」,而是「讓多個系統共用同一個身分提供者(Identity Provider, IdP)與同一組登入結果」。對使用者來說,這表示少輸入一次密碼;對系統來說,這表示各服務不必各自處理帳密驗證、找回密碼、風險控管與登入狀態同步。{:.block-tip}

1.1 What problems does SSO solve?

  • 使用者在多個產品間切換時,不需要重複登入
  • 每個應用程式不必自己保存密碼驗證邏輯
  • 登入政策可以集中在 IdP 上做,例如 MFA、裝置信任、封鎖風險帳號

1.2 SSO’s main roles

  • IdP:真正處理登入的人,像是 Google、Microsoft Entra ID、Auth0、Keycloak
  • RP / Client:想要使用這個登入結果的應用程式
  • Session:IdP 或應用程式內部保存的登入狀態

如果把它講得更直白一點,SSO 其實就是「把登入這件事外包給一個大家都信任的地方」。後面的 OIDC 只是把這個外包流程標準化。

1.3 A typical SSO flow

  1. 使用者打開網站,網站發現自己沒有 session
  2. 網站把使用者導向 IdP 的登入頁
  3. 使用者在 IdP 完成驗證與同意
  4. IdP 把結果帶回應用程式
  5. 應用程式建立自己的 session,之後就不用再問 IdP 一次

2. JWT format

JWT 是 OIDC 裡最容易被誤解的部分。它看起來像一串亂碼,但本質上只是三段式字串:header、payload、signature。JWT 本身不是「加密格式」,大多數情況下它只是可驗簽的 JSON 封裝;如果需要保密,才會進到 JWE 的領域。這裡的定義可對照 RFC 7519。{:.block-warning}

2.1 JWT Structure

實際上 JWT 就只是兩個 JSON 物件(header 和 payload)加上一個簽章的一個結構,然後用 Base64url 編碼起來,最後用點號 . 串在一起, 這樣就能以 Text 的形式在 HTTP header 或 URL 裡面傳遞

header.payload.signature

實際上每一段都是 Base64url 編碼後的內容:

  • header:描述簽章演算法,例如 RS256
  • payload:放 claims,例如 isssubaudexp
  • signature:用來驗證內容沒有被改過
{
"alg":"RS256",
"typ":"JWT"
}
{
"iss":"https://idp.example.com",
"sub":"248289761001",
"aud":"client-id-123",
"exp":1910000000,
"iat":1909996400
}

2.2 Common Claims

  • iss:這顆 token 是誰發的
  • sub:這顆 token 指向哪個使用者
  • aud:這顆 token 是發給誰看的
  • exp:過期時間
  • iat:簽發時間
  • nonce:把登入請求和回應綁在一起,避免 replay

其中最重要的是 iss + sub 的組合。iss 先把識別範圍限定在同一個 issuer,sub 再當作這個 issuer 底下穩定的使用者識別子;emailnamepreferred_username 都不適合直接當主鍵。這個判斷可直接對照 OpenID Connect Core 的 ID Token 與 subject 定義。{:.block-tip}

2.3 What to Check When Verifying JWTs

  1. 先驗簽章,確認內容真的是該 IdP 發的
  2. 再驗 issaudexp
  3. 如果流程有 nonce,也要比對 nonce
  4. 不要只要拿到 payload 就直接相信裡面的資料

換句話說,JWT 不是拿來「讀一讀就算了」,而是拿來「驗完之後再使用」。


3. OIDC

OIDC 的角色是把 OAuth 2.0 的授權流程補上身分語意,讓系統不只知道「有沒有權限」,也知道「現在登入的人是誰」。

OIDC 是建立在 OAuth 2.0 上的 identity layer。OAuth 2.0 本來主要處理「授權」,也就是你能不能存取資源;OIDC 則是在這個流程上再補上「身份驗證」,讓 Client 可以知道使用者到底是誰。OpenID Connect Core 直接把它定義成 OAuth 2.0 上的一層簡單 identity layer。{:.block-tip}

3.1 The Difference Between OIDC and OAuth 2.0

  • OAuth 2.0 回答的是「這個 client 能不能拿到資源」
  • OIDC 回答的是「這個 user 是不是已經被驗證過」
  • OAuth 2.0 的結果通常是 access_token
  • OIDC 會多出 id_token

這也是為什麼 OIDC 的 authentication request 會包含 scope=openid。少了 openid,流程就只是 OAuth 2.0 授權,不是 OIDC 的身份驗證流程。可對照 OpenID Connect Core

3.2 Authorization Code Flow

對後端來說,OIDC 最常見、也最值得講清楚的就是 Authorization Code Flow。它的優點是 token 不會直接暴露在 user agent 裡面,因此通常是現代 web app 的預設選擇;相較之下,implicit flow 主要是歷史包袱,現在多半不建議新系統再採用。{:.block-warning}

GET /authorize?response_type=code&scope=openid%20email%20profile&client_id=client-id-123&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&state=abc123&nonce=n-0S6_WzA2Mj HTTP/1.1
Host: idp.example.com
  1. Client 把使用者導向 /authorize
  2. IdP 驗證使用者,並確認 consent
  3. IdP 把 code 帶回 callback
  4. Client 用 code/token 換回 id_tokenaccess_token
  5. Client 驗證 id_token 後,再建立自己的 session
POST /token HTTP/1.1
Host: idp.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&client_id=client-id-123&client_secret=secret

3.3 Key OIDC Endpoints

  • /.well-known/openid-configuration:discovery,用來取得端點與支援能力
  • /authorize:使用者登入入口
  • /token:用 code 換 token
  • /userinfo:用 access_token 拿更多 user claims
  • jwks_uri:拿公開金鑰來驗簽 id_token

3.4 What the Backend Should Verify

OIDC 規格要求 client 驗證 iss 必須和 discovery 回來的 issuer 一致,aud 必須包含自己的 client id,exp 不能過期,nonce 也要對得上。這些不是裝飾,而是避免 token substitution、replay、和錯誤受眾使用的核心防線。{:.block-tip}

最實際的做法是:

  1. 用 library 讀 discovery document
  2. 用 JWKS 驗 id_token
  3. 檢查 issaudexpnonce
  4. 驗證通過後,只把你真正需要的 user identity 存進自己系統的 session

4. NextAuth

NextAuth 是把 OIDC 接進 Next.js 的實作層,不是把協定知識拿掉;先懂流程,再讓它幫你省掉重複的程式碼。

NextAuth 的價值是把 OIDC / OAuth 的細節包成 Next.js 好用的 auth 層。它幫你處理 redirect、callback、session 與 token 存取,但它不是把所有安全責任都吃掉;你還是要知道 provider 怎麼設定、callback 裡面有哪些資料可以用,以及 session 最後會長什麼樣子。{:.block-tip}

4.1 How It Usually Connects to OIDC

如果 provider 本身是 OIDC 相容的,NextAuth 通常會先讀 wellKnown 的 discovery document,再依 provider 回傳的端點與能力完成授權流程,而不是手動拼 /authorize/token/userinfo。這個用法可對照 NextAuth OAuth providersNextAuth Callbacks 的說明。

import NextAuth from "next-auth"

export const authOptions = {
	providers: [
		{
			id: "my-oidc",
			name: "My OIDC",
			type: "oauth",
			wellKnown: "https://idp.example.com/.well-known/openid-configuration",
			clientId: process.env.OIDC_CLIENT_ID,
			clientSecret: process.env.OIDC_CLIENT_SECRET,
			authorization: {
				params: {
					scope: "openid email profile",
				},
			},
			idToken: true,
			checks: ["pkce", "state"],
		},
	],
	callbacks: {
		async jwt({ token, account }) {
			if (account) {
				token.accessToken = account.access_token
			}
			return token
		},
		async session({ session, token }) {
			session.accessToken = token.accessToken
			return session
		},
	},
}

export default NextAuth(authOptions)

4.2 Why Callbacks Matter

NextAuth 最實用的地方,不只是「幫你登入」,而是它允許你在 jwt()session() 之間傳遞你真的需要的資料。官方文件的建議很直接:如果你要把 access token 或 user id 帶到前端,就先放進 JWT,再從 session callback 轉出去。{:.block-tip}

  • jwt():登入時建立 token,之後每次 session 更新也會走
  • session():決定前端最後拿到哪些欄位
  • signIn():可以做允許名單、封鎖名單、網域限制
  • redirect():控制登入後跳轉的位置

4.3 How to Think About NextAuth in Practice

我會把 NextAuth 視為「把 OIDC 接進 Next.js 的 adapter」,而不是「取代 OIDC 知識的魔法盒」。如果你不清楚 id_tokenaccess_tokenwellKnownpkcesession 的差別,NextAuth 只是讓錯誤更晚爆出來而已。真正穩定的方式是:先把 OIDC 流程想清楚,再用 NextAuth 去實作。

Last Edit

06-09-2026 23:44

results matching ""

    No results matching ""

    , software, authentication, oauth, jwt, nextauth, sso