OAuth 2.0 授權伺服器

讓第三方應用程式向你的使用者申請授權。這與「使用 Google 登入你的網站」是不同的功能。

Saavo 內建 OAuth 2.0 授權伺服器,讓你能將產品發展成開放平台。核心流程是由第三方應用程式引導使用者登入並授權,再以授權碼交換存取權杖,進而呼叫你開放的 API。

請注意,這與 Google 登入等社群帳號登入不同。社群登入是讓「外部身分進入你的系統」,Saavo 的授權伺服器則是讓「外部應用程式存取你的使用者資源」。

使用時,需要在開放的 API 中執行檢查,確認請求的權杖權限有效,且操作在允許的授權範圍內。

既有功能

範本初始化後,即可使用下列功能:

用途預設入口預設狀態
授權/oauth/authorizeoauth2.enableService 預設為 true
登入後繼續/oauth/authorize/continue已啟用
同意頁面/oauth/authorize/consent已啟用
交換權杖POST /oauth/token已掛載
撤銷權杖POST /oauth/token/revoke已掛載
取得授權後的使用者資訊GET /oauth/current-user需要 user scope
管理第三方用戶端/dashboard/oauth-server管理員可建立用戶端

範本預設支援授權碼、重新整理權杖與用戶端認證三種授權類型。其中,授權碼流程支援 PKCE 擴充,並強制使用 S256 演算法產生 Code Challenge。管理員建立 OAuth 用戶端時,可以設定回呼位址,並指定該用戶端允許的授權類型(Grant Type)與授權範圍(Scope)。

預設授權範圍位於 config/scopes.ts:

Scope意義
user存取使用者資訊
basic範例的基本存取範圍

角色描述使用者本身擁有的權限,Scope 則描述某個存取權杖可以執行的操作。即使目前使用者是管理員,第三方應用程式取得的權杖也可能只有 basic Scope,因此只能存取該 basic Scope 允許的 API。

OAuth 授權同意頁面

與 Google 登入有什麼不同

授權伺服器社群登入
目的第三方應用程式存取你的 API使用者登入你的網站
頁面/oauth/authorize*/auth/login 等
回呼/交換權杖/oauth/token/api/auth/oauth2/google、/github
設定deploy.oauth2、scopes.tsGOOGLE_* / GITHUB_* 環境變數

/api/auth/current-user 讀取網站的 Session,/oauth/current-user 則讀取第三方應用程式的存取權杖,請勿混用。

先體驗授權流程

如果產品暫時不對第三方開放,仍建議先到後台查看用戶端管理頁面,避免上線後才發現預設開關已啟用。

用戶端管理頁面

完整操作一次授權碼流程:

管理員建立用戶端,填寫 Redirect URI 與允許的 scope
↓
第三方應用程式開啟 /oauth/authorize
↓
使用者登入(若尚未登入)
↓
在同意頁面確認權限
↓
帶回授權碼
↓
POST /oauth/token 交換 access token 與 refresh token

預設授權碼有效期間為 15 分鐘,存取權杖為 1 天,重新整理權杖為 30 天。重新整理權杖會輪替。

提示

oauth2.enableService 只決定是否開放授權相關頁面,關閉後,/oauth/token 等 API 仍然可用。如果產品暫時不對第三方開放,建議不要建立任何用戶端,也不要在業務 API 中使用這些權杖。

設定 OAuth 2.0 授權伺服器

授權伺服器的相關設定,位於 config/deploy.ts 的 oauth2 區段與 config/scopes.ts。

是否開放 OAuth 2.0 服務

預設設定如下:

oauth2: {
    enableService: true,
    issuer: 'saavo',
    requiresPKCE: true,
    requiresS256: true,
}

產品初始化後,建議及早修改 issuer。這個值會寫入存取權杖的宣告,用來識別權杖的簽發者。

如果產品不需要提供開放平台功能,可以將 enableService 設為 false,並且不建立 OAuth 用戶端。請注意,目前這個開關只控制相關頁面是否啟用,不會停止 OAuth API 本身。

如果希望完全停用授權伺服器,也需要停止掛載 /oauth 相關 API 路由。

定義第三方可申請的授權範圍

在 config/scopes.ts 新增自己的 scope,並填寫名稱與說明。這些文案會顯示在同意頁面。

第三方實際取得的權限,是請求的 scope 與用戶端允許的 scope 兩者的交集。只在設定中新增 scope 還不夠,也需要在後台將該 scope 授予對應用戶端。

設定授權範圍的升級提示

config/upgrade.ts 預設是空物件。當使用者缺少某項權益時,可以在這裡設定升級卡片,讓同意頁面引導使用者購買更高階的方案。

沒有升級需求就保持空白,不要將計費邏輯寫入以授權碼交換權杖的 API。

準備授權服務

OAuth 權杖的處理會用到專案的 SAAS_SECRET。依本文件的部署流程,同一專案的 .env 與 .env.production 必須保留相同的既有金鑰,不要在部署時臨時另產生一把。獨立的新專案應使用自己的金鑰,也不要將其他專案的金鑰複製過來。詳見保留既有的 SAAS_SECRET。

第三方用戶端使用的 Redirect URI,必須與管理後台登記的位址完全一致。本機開發與正式環境通常使用不同網域,可以分別建立對應環境的 OAuth 用戶端,也可以為同一個用戶端設定多組允許的回呼位址。

在業務 API 中驗證存取權杖

只有主動對第三方開放 API 時,才需要在業務路由中檢查 OAuth 權杖。

保護資源的基本原則是:

先檢查權杖的 scope,再視需要確認該使用者是否仍擁有某個角色或使用權益。不要將使用者的 admin 角色寫入權杖。

const result = await authz.authorizeOAuthRequest(c, {
    scope: 'basic',
});

if (!result.success) {
    return c.json(result, 401);
}

如果這個 API 還要求使用者已購買某個產品:

const result = await authz.authorizeOAuthRequest(c, {
    scope: 'user',
    entitlement: 'starter_download',
});

也可以個別使用:

await authz.hasScopes(c, 'user')
await authz.hasAnyScope(c, ['user', 'basic'])

第三方應用程式透過 OAuth2 存取資源時,應只提供符合目前 Scope 的資料。範本提供的 GET /oauth/current-user 只是範例,即使通過 user Scope 檢查後會回傳部分使用者權益資訊,也不代表自己的資源 API 應照搬全部欄位。正式開放 API 時,應依實際業務決定哪些使用者資訊、角色或權益可以提供給第三方。

OAuth2 主要用於第三方用戶端存取 API。網站本身的頁面與第一方 API,仍繼續使用 Session 與 authenticatedGuard,不需要改用 Bearer Token 驗證。這兩種身分驗證方式適用於不同情境,不建議混用。

另外,透過 client_credentials 取得的權杖不代表任何特定使用者,因此不能依賴使用者角色、權益或其他個人狀態。它較適合服務之間的機器存取,這類 API 應僅依權杖的 Scope 決定是否允許存取。

提示

需要使用者授權時,使用授權碼與 PKCE 流程。服務之間不代表特定使用者的存取,才使用用戶端認證。重新整理權杖用來延續已核准的授權,範本也支援這個流程,不應將它歸類為不安全的授權類型。避免使用隱含式流程,也不要使用資源擁有者密碼憑證流程,相關說明請見 OAuth 安全最佳實務。

上線檢查

授權伺服器會將使用者權限提供給第三方,上線前建議至少確認:

  • issuer 已改為自己的產品識別值。
  • SAAS_SECRET 保留此專案既有的值,且 .env 與 .env.production 一致。
  • 若不開放平台,未建立正式用戶端,且已關閉授權頁面。如果要求 API 也無法存取,已停止掛載 /oauth 路由。
  • 若開放平台,已使用授權碼與 PKCE 完成登入、同意、交換權杖、重新整理與撤銷流程。
  • 用戶端 Redirect URI 已指向正式環境的回呼位址,不再使用本機位址。
  • 業務 API 檢查的是 scope,而不是使用者是否為管理員。
  • 權杖無法取得未申請的 scope。
  • 授權範圍的文案已換成適合自己產品的說明。

建議使用專門的測試用戶端完成上述流程,不要以網站管理員的 Session 代替權杖測試。

常見問題

接下來

依接下來要開發的功能,可以繼續閱讀:

大多數產品若只是提供自己的 SaaS 服務,不需要將授權伺服器視為核心功能。

只有確實要建立開放平台時,再以 scope 保護第三方 API。