cd okta-client-dpop-project
npm ci

在您最喜歡的集成開發環境中打開該項目。該項目包括 Okta 的客戶端身份驗證 SDK、一個登錄按鈕、一個通過調用 OIDC/userinfo端點顯示用戶信息的配置文件路由,以及一個調用Okta 的用戶 API 的路由。這兩個 HTTP 請求都需要訪問令牌,因此我們將跟蹤這兩個調用的請求和響應。

React 和 Vue 項目說明

React和Vue項目需要做一些改動。更改配置文件組件,調用oktaAuth.token.getUserInfo()并顯示 JSON 輸出。添加對Okta用戶API https://{yourOktaDomain}/api/v1/users 的調用。稍后您將替換域名。您可能需要創建一個新的用戶組件(和路由)來匹配 Angular 示例。

使用React和Vue?的 SDK 參考文檔。

您需要設置一個身份驗證配置來為項目提供服務。現在就開始吧。

在應用程序中添加 OAuth 2.0 和 OpenID Connect (OIDC)

在本項目中,您將使用 Okta 安全地處理身份驗證和授權。Okta API 具有內置的 DPoP 支持 – 多么安全和方便!我們將通過調用 Okta 的 API 在客戶端應用程序中嘗試使用 DPoP。

React 和 Vue 項目說明

替換兩個重定向 URI,使其與應用程序的端口和回調路由相匹配。您可以在項目的 README 文件中找到這兩個 URI。按照 README 中的說明將發行方和客戶端 ID 添加到應用程序中。發行人使用https://{yourOktaDomain}格式。注意這與啟動代碼不同。

開始之前,您需要注冊一個免費的Okta開發者賬號。請先安裝Okta的命令行工具(CLI),然后通過執行okta register命令來創建一個新的賬戶。如果您已有賬戶,可以直接使用okta login來登錄。登錄后,執行okta application create命令。您可以選擇使用默認的應用程序名稱,或者根據實際情況進行修改。在選擇應用類型時,請選取“單頁面應用(Single Page App)”,然后按回車繼續。

請設置回調(redirect URI)為http://localhost:4200/login/callback,并把注銷回調(logout redirect URI)設為http://localhost:4200。關于Okta CLI,它具備哪些功能?

Okta CLI能夠在您的Okta組織內創建一個OIDC單頁面應用程序。它會添加您指定的回調地址,并默認給予Everyone組訪問權限。同時,它還會將http://localhost:4200添加到可信的來源列表中。完成這些操作后,您將看到如下的輸出結果:

Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6

注意:您也可以使用 Okta 管理控制臺創建應用程序。

請注意 “簽發人 "和 “客戶端 ID“。在即將進行的身份驗證配置中,您將需要這些值。

只需在 Okta 管理控制臺中進行一次手動更改。在您的 Okta 應用程序中添加刷新令牌授權類型。打開一個瀏覽器標簽,登錄到您的Okta 開發者賬戶。導航到應用程序>應用程序,找到您創建的 Okta 應用程序。選擇名稱以編輯應用程序。找到 “常規設置“部分,然后按 “編輯“按鈕添加授權類型。激活刷新令牌復選框,然后按保存

打開 Okta 管理控制臺。您可以繼續在那里進行更改。

我已經添加了Okta Angular和Okta Auth JS庫,以便將我們的 Angular 應用程序與 Okta 身份驗證連接起來。

在集成開發環境中,打開src/app/app.config.ts,找到OktaAuthModule.forRoot()配置。將{yourOktaDomain}{yourClientID}替換為 Okta CLI 中的值。

為調用Okta API配置OAuth的權限范圍(Scopes)

我們調用的是 Okta API,因此必須添加所需的 OAuth 作用域。

在Okta管理控制臺中,導航到您的Okta應用程序中的 “Okta API作用域選項卡。找到okta.apps.readokta.users.read.self并分別點擊?? Grant按鈕。

打開 src/app/users/users.component.ts ,找到列出用戶的調用: https://{yourOktaDomain}/api/v1/users 。我們在這里采用了快捷方式,例如在本演示項目中直接在組件中調用 API。在具有生產質量的 Angular 應用程序中,請確保按照最佳實踐構建應用程序,以便添加自動化測試并快速排除故障。

{yourOktaDomain}替換為您的 Okta 域名。

React 和 Vue 項目說明

將兩個作用域添加到應用程序的 OIDC 配置中。搜索 “作用域 “并將數組更改為

scopes: ['openid', 'profile', 'email', 'offline_access', 'okta.users.read.self', 'okta.apps.read'],

替換您在上一節中添加的 Okta 用戶 API 調用中的{yourOktaDomain}

運行程序啟動應用程序:

npm start

打開瀏覽器,查看您的應用程序。同時打開瀏覽器的開發者工具,顯示控制臺和網絡請求的調試信息。因為我使用的是Chrome瀏覽器,所以會打開DevTools。在控制臺和網絡標簽頁中,開啟日志保存功能。在控制臺標簽頁中,通過打開設置(齒輪圖標)菜單,就能找到保存日志的選項。

接下來,我們要確保您能夠登錄系統,調用/userinfo端點查看用戶信息,以及調用Okta用戶API。您將通過授權碼流程,被重定向到Okta進行身份驗證。
一旦您向身份提供者證明了身份,授權服務器就會將您重定向回應用程序。重定向的URI會包含授權碼。Okta的SDK(OIDC客戶端庫)會用這個授權碼去/token端點換取訪問令牌。

登錄成功后,Angular應用會展示“Profile”和“Users”這兩個路由。訪問這些路由會觸發對/userinfo和Users API的調用。如果您能夠順利訪問這些路由,并且沒有遇到任何HTTP請求錯誤,那么您就可以繼續后續操作了!

核查OAuth 2.0的Bearer令牌并手動索取資源

登錄完成后,您將獲得OAuth 2.0的訪問令牌和OIDC的ID令牌。Okta會將這些令牌保存在瀏覽器的存儲空間里。在DevTools中,切換到“應用程序”標簽來查看瀏覽器存儲的數據。Okta Auth JS默認情況下會將令牌保存在本地存儲中,當然,您也可以根據自己應用的需求進行相應的配置。在本地存儲中展開對應的應用程序項,然后展開名為“okta-token-storage”的鍵,您就能看到存儲的令牌和令牌的元數據。其中,“tokenType”屬性表示的是攜帶者(Bearer)。

Browser local storage showing Okta's token storage. The access token has the token in JWT format as well as scopes, claims, and token type as top-level properties.

讓我們看看應用程序中的 API 調用。導航到兩個路徑。在 “網絡“選項卡中,你會看到初始的/token/userinfo 和 Users API 請求。

讓我們檢查一下用戶 API 請求。

Network requests showing calls to token, userinfo, and users endpoints. the users api call shows a 200 OK status code.

請求包含授權標頭,其中包含令牌方案和訪問令牌。您可以看到格式為Bearer <access_token>

Users API request headers showing the Authorization header.

持有令牌的實體可以合法請求資源。讓我們嘗試在另一個客戶端中使用令牌,并模仿攻擊者在成功捕獲令牌后可能采取的行動。

備注

訪問令牌很快就會過期。如果接下來的步驟時間過長,可能會出現401 未授權。如果出現這種情況,請使用較新的訪問令牌重復上述步驟,在配置文件和用戶路由之間導航,以觸發對 API 的調用。它會提示 OIDC 客戶端(Okta Auth JS SDK)更新過期令牌。

從瀏覽器中復制令牌,并仔細檢查是否捕獲了整個令牌。打開 HTTP 客戶端并運行以下 HTTP 請求,替換{yourOktaDomain}{yourAccessToken}

GET https://{yourOktaDomain}/api/v1/users HTTP/1.1
Authorization: Bearer {yourAccessToken}

如果使用 curl,請添加 verbose 標志,以查看請求和響應頭信息:

curl -v --header "Authorization: Bearer {yourAccessToken}" https://{yourOktaDomain}/api/v1/users

即使 HTTP 客戶端與授權服務器發送令牌的客戶端(示例應用程序)不是同一個客戶端,調用也會成功。

讓我們用相同的訪問令牌調用另一個端點,即 Okta 應用程序端點。運行以下 HTTP 請求,替換{yourOktaDomain}{yourAccessToken}

GET https://alisa.oktapreview.com/api/v1/apps HTTP/1.1
Authorization: Bearer {yourAccessToken}

即便您通過不同的客戶端發起調用,只要Okta應用程序具備okta.apps.read權限,并且OIDC配置中的范圍(Scope)設置正確,像您之前在調用用戶API時看到的那樣,調用也會成功。您可能會覺得這有很多限制,確實。當您對頂層Okta組織中的資源(例如Okta應用程序列表)進行API請求時,Okta會施加許多保護措施。

這個例子展示了為管理員等具有特權的用戶頒發的令牌是多么的強大,同時也是脆弱的。任何持有這些令牌的人都能夠發起相同的請求,哪怕他們是攻擊者。

回到應用程序,您需要先退出登錄以清除已驗證的會話和令牌。我們正在進行一些更改,所以需要您重新登錄。

使用安全編碼技術保護您的網絡應用程序

所有網絡應用都需采用安全的編碼技術來防范攻擊、漏洞和惡意利用。由于公共客戶端將令牌存儲在用戶設備上,因此需要采取周全的安全措施。

請閱讀本系列文章的四個部分,深入了解有關SPA(單頁應用)網絡安全和Angular安全實踐的信息:確保您的SPA免受安全威脅。

掌握網絡安全的基本概念,并學習如何將這些基礎應用于保護您的單頁應用。

無論應用程序使用的是匿名令牌還是DPoP,都必須遵循安全的編碼實踐。DPoP雖不能阻止攻擊者盜取令牌,但可以限制令牌的使用范圍。
DPoP利用非對稱加密技術來驗證令牌的所有權,因此必須防止密鑰泄露或被未經授權的使用。一旦攻擊者獲取了私鑰,他們就能生成有效的憑證。

讓我們將應用程序升級至DPoP,并再次嘗試執行這些HTTP請求。

將單頁應用(SPA)升級以采用DPoP。

在瀏覽器中打開 Okta 管理控制臺,導航到應用程序>應用程序。找到此項目的 Okta 應用程序。在 “常規“選項卡中找到 “常規設置“部分并按 “編輯“。選中需要在令牌請求中使用 DPoP 標頭的 “擁有證明“復選框。按保存。退出 Okta 管理控制臺。

如果在不修改任何代碼的情況下再次嘗試登錄,就會在網絡選項卡中看到/token請求出錯:

HTTP/1.1 400 Bad Request

{
"error": "invalid_dpop_proof",
"error_description": "The DPoP proof JWT header is missing."
}

向受 DPoP 保護的資源提出的所有 HTTP 請求(包括/token請求)都需要證明。我們必須在 OIDC 配置中啟用 DPoP。

作為 OIDC 配置的一部分,Okta Auth JS SDK 有一個用于 DPoP 的配置屬性。在集成開發環境中,打開src/app/app.config.ts,找到OktaAuthModule.forRoot()配置。添加dpop: true屬性。您的 OIDC 配置將如下所示:

{
issuer: ...,
clientId: ...,
redirectUri: ...,
scopes: ['openid', 'profile', 'offline_access', 'okta.users.read.self', 'okta.apps.read'],
dpop: true
}

應用程序重建并在瀏覽器中重新加載后,確保已打開調試工具,然后登錄。

跟蹤需要 DPoP nonce 的令牌請求

登錄時,你會看到對/token端點的初始調用失敗。

Network requests showing the first call fails with HTTP status 400 and the second call succeeds.

查看調用的請求頭,你會發現一個名為DPoP的頭部,里面包含了一個JWT格式的DPoP憑證,這意味著我們可以對其內容進行解碼和檢查。你可以使用一些可靠的在線工具(例如JWT.io的調試器),或者在本地對JWT的頭部和載荷部分進行Base64解碼。在JWT格式中,從開始到第一個”.”字符之間的內容是頭部,兩個”.”字符之間的內容是載荷。

頭部包含了令牌類型、dpop+jwt、加密算法以及與這個憑證相關的加密密鑰信息。載荷則包含了最基本的HTTP信息和其他屬性,這些都是為了防御針對令牌的攻擊手段。

{  
"alg": "RS256",
"typ": "dpop+jwt",
"jwk": { /* Key information in JSON Web Key format */ }
}

{
"htm":"POST",
"htu":"https://{yourOktaDomain}/oauth2/v1/token",
"iat":1724685617,
"jti": "e84a...283bbf",
}

為什么最初調用/token會失敗?這是因為 Okta 需要額外的握手來提高安全性。/token調用需要一個 DPoP nonce,Okta 在 DPoP 證明中提供了這個 nonce。在響應第一次/token調用時,Okta 會返回標準的 DPoP nonce 錯誤和DPoP-Nonce響應頭,其中包含客戶端納入證明的 nonce。

HTTP/1.1 400 Bad Request
DPoP-Nonce: "SVD....ubNc"

{
"error": "use_dpop_nonce",
"error_description": "Authorization server requires nonce in DPoP proof."
}

Okta 的 Auth JS SDK 內置支持DPoP-Nonce錯誤。請看 DPoP 證明令牌在成功的/token請求中的有效載荷。有效載荷包括第一次調用返回的 nonce。

{
"htm":"POST",
"htu":"https://{yourOktaDomain}/oauth2/v1/token",
"iat":1724685617,
"jti": "e852...28396",
"nonce":"SVD....ubNc"
}

令牌請求成功,我們現在有了 DPoP 訪問令牌。

使用 DPoP 標頭請求資源

在應用程序中,由于 SDK 支持 DPoP 資源請求,因此導航查看個人資料會成功。當導航到調用 Okta 用戶 API 的 “用戶 “路由時,您會看到一個錯誤。

HTTP 響應包括呼叫出錯的原因。

HTTP/1.1 400 Bad Request
WWW-Authenticate: Bearer authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.users.read.self", error="invalid_request", error_description="The resource request requires a DPoP proof.", resource="/api/v1/users"

目前調用用戶API的代碼使用的是帶有Bearer方案的授權頭來添加訪問令牌,但這種做法不適用于DPoP。我們需要在HTTP請求中加入DPoP憑證,并更改方案。

請在集成開發環境中打開認證攔截器。相關代碼位于src/app/auth.interceptor.ts文件中。

React 和 Vue 項目說明

找到添加到請求用戶中的代碼,并在項目中加入 Angular 說明,以添加 DPoP 證明頭和DPoP方案。

攔截器會進行檢查,以確保只將訪問令牌添加到允許的來源。攔截器代碼修改如下:

export const authInterceptor: HttpInterceptorFn = (req, next, oktaAuth = inject(OKTA_AUTH)) => {
let request = req;
const allowedOrigins = ['/api'];
if (!allowedOrigins.find(origin => req.url.includes(origin))) {
return next(request);
}
};

我們需要證明和授權頭。我們將使用 Okta Auth JS 生成這兩個文件。SDK 方法需要我們打算調用的 HTTP 方法和 URI。URI 不應包含查詢參數或片段。
SDK 方法會返回一個對象,其中包含與標頭及其值相匹配的屬性,因此我們可以使用傳播運算符來填充 DPoP 所需的標頭。

更改攔截器,使其與下面的代碼一致。

import { DPoPHeaders } from '@okta/okta-auth-js';
import { defer, map, switchMap } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next, oktaAuth = inject(OKTA_AUTH)) => {
// allowed origin check

const url = new URL(req.url);

return defer(() => oktaAuth.getDPoPAuthorizationHeaders({url: ${url.origin}${url.pathname}, method: req.method})).pipe( map((dpop: DPoPHeaders) => req.clone({ setHeaders: { ...dpop } })), switchMap((request) => next(request)) ); };

現在,只要您登錄并調用用戶API,就能獲取使用DPoP的Okta組織中的用戶列表。

手動申請受 DPoP 保護的資源

之前,我們模擬了訪問令牌被盜的情況,并用它來請求其他資源。您使用JWT令牌調用了Okta Apps API,查看了您的Okta組織中包含的所有應用程序列表。如果我們在需要DPoP的應用程序接口上再試一次,結果會如何呢?

打開DevTools中的網絡標簽頁,找到對/users的調用。這個HTTP調用需要DPoP證明和訪問令牌。嘗試發起HTTP請求:

curl -v --header "Authorization: DPoP {yourAccessToken}" --header "DPoP: {yourDPoPProof}" https://{yourOktaDomain}/api/v1/apps

應用程序接口拒絕了您的請求!返回錯誤信息,說明 DPoP 證明無效:

HTTP/1.1 400 Bad Request
WWW-Authenticate: DPoP algs="RS256 RS384 RS512 ES256 ES384 ES512", authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.apps.read", error="invalid_dpop_proof", error_description="'htu' claim in the DPoP proof JWT is invalid."

如果攻擊者成功捕獲了證明和令牌,他們可能只能發出相同的請求。證明會限制對 HTTP 方法和 URI 的調用,使其他 HTTP 請求無效。

提出同樣的要求如何?

curl -v --header "Authorization: DPoP {yourAccessToken}" --header "DPoP: {yourDPoPProof}" https://{yourOktaDomain}/api/v1/users

應用程序接口拒絕了您的請求!您仍然會收到一個錯誤,說明 DPoP 證明無效:

HTTP/1.1 400 Bad Request
WWW-Authenticate: DPoP algs="RS256 RS384 RS512 ES256 ES384 ES512", authorization_uri="http://{yourOktaDomain}/oauth2/v1/authorize", realm="http://{yourOktaDomain}", scope="okta.users.read.self", error="invalid_dpop_proof", error_description="The DPoP proof JWT has already been used.", resource="/api/v1/users"

證明還有另外兩種保護機制:JWT 唯一標識符(jit)和簽發時間(iat)。當資源服務器執行jit聲明時,它會跟蹤以前的調用,以防止證明重復使用。因此,攻擊者無法重放他們竊取的證明和訪問令牌。DPoP 規范并不要求執行 JWT ID。另一種保護機制是證明簽發時間戳,即iatclaim。資源服務器會檢查證明的簽發時間,如果超過資源服務器確定的某個閾值,服務器就會拒絕請求。

在瀏覽器應用程序中存儲加密密鑰

我們必須確保在單頁應用(SPA)中安全地保管密鑰集,防止攻擊者竊取。一旦攻擊者獲得密鑰集,他們就能冒充你,發起受DPoP保護的請求。

幸運的是,Okta SDK采用了多種技術來降低密鑰集被劫持的風險,無需您進行額外的編碼工作。

本地存儲和會話存儲的安全性不足,因此我們將轉而使用IndexedDB進行存儲。IndexedDB通常用于存儲大量數據,但它內置了一些安全機制,能夠有效保護密鑰集。SubtleCrypto API能夠生成不可導出的密鑰,避免瀏覽器代碼將私鑰轉換成可攜帶的格式。IndexedDB將密鑰以CryptoKeyPairs對象的形式存儲,查詢結果返回的是對該對象的引用,而非原始密鑰。這樣,IndexedDB既能保護敏感的私鑰,又能通過WebCrypto API進行簽名驗證。

您可以按照以下步驟檢查鑰匙:

  1. 導航至 DevTools 中的應用程序選項卡
  2. 在 “存儲 “側導航下展開IndexedDB
  3. 展開OktaAuthJs>DPoPKeys

缺點是 IndexedDB API 比其他瀏覽器存儲 API 更難使用。由于 IndexedDB 數據是持久的,因此我們必須手動清理鍵值。如果用戶明確注銷,SDK 會處理清理工作,但我們不能保證用戶總是會注銷。
我們可以在簽到前清理鑰匙。

打開src/app/app.component.ts,找到signIn()方法。

React 和 Vue 項目說明

找到項目調用signInWithRedirect()方法的代碼,并按照針對 Angular 項目的說明進行操作。

signIn()方法的第一步中添加清除密鑰的調用:

public async signIn() : Promise<void> {
await this.oktaAuth.clearDPoPStorage(true);
await this.oktaAuth.signInWithRedirect();
}

請使用最新的主流瀏覽器來安全地處理令牌

在JavaScript應用中生成和保存加密密鑰需要一個支持高級功能的瀏覽器。當前主流的現代瀏覽器都已經支持DPoP所需的API。如果你的應用服務的用戶使用的瀏覽器版本較舊或存在較多問題,請確保檢查這些瀏覽器是否具備所需功能。

Auth JS SDK提供了一個方法來檢查瀏覽器是否支持DPoP,即authClient.features.isDPoPSupported()。你可以在應用的啟動或初始化階段加入這項檢查。

請記住,即便你不采用DPoP,現代瀏覽器也內置了更多安全特性。為了安全起見,請及時更新瀏覽器,并盡可能使用安全性更高的瀏覽器。

原文鏈接:https://developer.okta.com/blog/2024/09/10/angular-dpop-jwt

上一篇:

OpenID Connect (OIDC)是什么及其應用

下一篇:

API安全指南:如何保護數據免受攻擊?
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費