1. 引言
在本教程中,我們將討論使用 Proof Key for Code Exchange (PKCE) 用於 OAuth 2.0 公共客户端的應用。
2. 背景
OAuth 2.0 公有客户端,例如使用 單頁應用程序 (SPA) 或利用 授權碼流程 的移動應用程序,容易受到授權碼截獲攻擊。惡意攻擊者如果在不安全的網絡上,能夠截獲授權端點的授權碼,則可能導致攻擊。
如果攻擊者能夠獲取授權碼,它就可以用來獲取訪問令牌。一旦攻擊者擁有訪問令牌,它就可以像合法用户一樣訪問受保護的應用程序資源,從而嚴重損害應用程序。例如,如果訪問令牌與金融應用程序相關聯,攻擊者可能會獲得敏感的應用程序信息。
2.1. OAuth 授權碼截獲攻擊
本節將討論 OAuth 授權碼截獲攻擊的發生方式: 上述圖表展示了惡意攻擊者如何濫用授權碼以獲取訪問令牌的流程。
- 一個合法的 OAuth 應用使用其 Web 瀏覽器發起 OAuth 授權請求流程,並提供所有必需的詳細信息。
- Web 瀏覽器將請求發送到授權服務器。
- 授權服務器將授權碼返回到 Web 瀏覽器。
- 在此時,惡意用户如果通信發生在不安全的通道上,則可能訪問授權碼。
- 惡意用户使用授權碼兑換以獲取訪問令牌,從授權服務器獲取訪問令牌。
- 由於授權碼有效,授權服務器會向惡意應用頒發訪問令牌。惡意應用可以濫用訪問令牌,代表合法的應用訪問受保護的資源。
Proof Key for Code Exchange 是一種旨在緩解該攻擊的 OAuth 框架擴展。
3. 使用 PKCE 進行 OAuth
PKCE 擴展包含以下額外步驟,應用於 OAuth 授權碼流程:
- 客户端應用程序發送兩個額外的參數code_challenge和code_challenge_method與初始授權請求一起
- 客户端還在下一步中發送code_verifier,用於在交換授權碼以獲取訪問令牌時
首先,一個啓用了 PKCE 的客户端應用程序選擇一個動態生成的密碼學隨機密鑰,稱為code_verifier。該code_verifier對於每個授權請求都是唯一的。根據RFC7636 規範,code_verifier值的長度必須在 43 到 128 個八字節之間。
此外,code_verifier只能包含字母數字 ASCII 字符和少數允許的符號。其次,code_verifier被轉換成code_challenge,使用支持的code_challenge_method進行轉換。目前支持的轉換方法是plain和S256。plain是一種無操作轉換,保持code_challange的值與code_verifier的值相同。S256方法首先對code_verifier生成 SHA-256 哈希值,然後對哈希值進行 Base64 編碼。
3.1. 防止 OAuth 代碼截獲攻擊
以下圖表展示了 PKCE 擴展如何防止訪問令牌被盜用:
- 一個合法的 OAuth 應用程序使用其 Web 瀏覽器發起 OAuth 授權請求流程,並同時包含code_challenge和code_challenge_method參數。
- Web 瀏覽器將請求發送到授權服務器,並存儲code_challenge和code_challenge_method參數,用於客户端應用程序。
- 授權服務器將授權碼返回到 Web 瀏覽器。
- 在此時,惡意用户如果通信發生在不安全的通道上,則可以訪問授權碼。
- 惡意用户嘗試使用授權碼 grant 進行訪問令牌的兑換,從授權服務器獲取訪問令牌。然而,惡意用户不知道需要發送的code_verifier。授權服務器拒絕對惡意應用程序的訪問令牌請求。
- 合法應用程序將code_verifier與授權 grant 一起提供,以獲取訪問令牌。授權服務器根據提供的code_verifier和之前從授權碼 grant 請求中存儲的code_challenge_method計算code_challenge。它將計算出的code_challange與之前存儲的code_challenge進行匹配。這些值始終匹配,客户端將獲得訪問令牌。
- 客户端可以使用此訪問令牌訪問應用程序資源。
4. 使用 Spring Security 實現 PKCE
從 6.3 版本開始,Spring Security 支持用於 Servlet 和 Reactive Web 應用程序的 PKCE。 但是,由於並非所有身份提供商都支持 PKCE 擴展,因此它默認未啓用。 對於公共客户端,PKCE 將自動使用,當客户端在不受信任的環境中運行時,例如本機應用程序或基於 Web 瀏覽器的應用程序,並且 client_secret 為空或未提供,以及 client-authentication-method 設置為 none。
4.1. Maven 配置
Spring Authorization Server 支持 PKCE 擴展。因此,為 Spring 授權服務器應用程序包含 PKCE 支持的簡單方法是包含 <em >spring-boot-starter-oauth2-authorization-server</em> 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.3.0</version>
</dependency>4.2. 註冊公共客户端
接下來,讓我們通過在 application.yml 文件中配置以下屬性來註冊一個公共單頁應用程序客户端:
spring:
security:
oauth2:
authorizationserver:
client:
public-client:
registration:
client-id: "public-client"
client-authentication-methods:
- "none"
authorization-grant-types:
- "authorization_code"
redirect-uris:
- "http://127.0.0.1:3000/callback"
scopes:
- "openid"
- "profile"
- "email"
require-authorization-consent: true
require-proof-key: true在上述代碼片段中,我們使用 client_id 為 public-client,並使用 client-authentication-methods 為 none 進行客户端註冊。 require-authorization-consent 要求用户在成功認證後,提供額外的同意,才能訪問 profile 和 email 範圍。 require-proof-key 配置可以防止 PKCE 降級攻擊。
啓用 require-proof-key 配置後,授權服務器不會允許任何惡意嘗試繞過 PKCE 流,而無需 code_challenge。 其餘配置是標準配置,用於使用授權服務器註冊客户端。
4.3. 授權服務器的安全配置
接下來,讓我們定義用於授權服務器的 SecurityFileChain 配置:
@Bean
@Order(1)
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.cors(Customizer.withDefaults())
.build();
}在上述配置中,我們首先應用授權服務器的默認安全設置。然後,我們應用 Spring Security 默認設置,用於 OIDC、CORS 和 OAuth2 資源服務器。現在,讓我們定義另一個 SecurityFilterChain 配置,該配置將應用於其他 HTTP 請求,例如登錄頁面:
@Bean
@Order(2)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize.anyRequest()
.authenticated())
.formLogin(Customizer.withDefaults());
return http.cors(Customizer.withDefaults())
.build();
}在本示例中,我們使用一個非常簡單的 React 應用程序作為公共客户端。該應用程序在 http://127.0.0.1:3000 上運行。授權服務器在不同的端口上運行,端口號為 9000。由於這兩個應用程序運行在不同的域名上,因此我們需要提供額外的 CORS 設置,以便授權服務器允許 React 應用程序訪問它:
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("http://127.0.0.1:3000");
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
我們正在定義一個 <em>CorsConfigurationSource</em> 實例,其中包含允許的源、頭部、方法和其他配置。請注意,在上述配置中,我們使用 IP 地址 127.0.0.1 而不是 <em>localhost</em>,因為後者是被禁止的。最後,讓我們定義一個 <em>UserDetailsService</em> 實例,用於在授權服務器中定義用户。
@Bean
UserDetailsService userDetailsService() {
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails userDetails = User.builder()
.username("john")
.password("password")
.passwordEncoder(passwordEncoder::encode)
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}通過上述配置,我們將能夠使用用户名 john 和 password 作為密碼進行身份驗證,以訪問授權服務器。
4.4 公共客户端應用
現在我們來討論公共客户端。為了演示目的,我們使用一個簡單的 React 應用作為單頁應用(SPA)。該應用使用 oidc-client-ts 庫進行客户端 OIDC 和 OAuth2 支持。該 SPA 應用配置如下:
const pkceAuthConfig = {
authority: 'http://127.0.0.1:9000/',
client_id: 'public-client',
redirect_uri: 'http://127.0.0.1:3000/callback',
response_type: 'code',
scope: 'openid profile email',
post_logout_redirect_uri: 'http://127.0.0.1:3000/',
userinfo_endpoint: 'http://127.0.0.1:9000/userinfo',
response_mode: 'query',
code_challenge_method: 'S256',
};
export default pkceAuthConfig;
權限服務器配置了 Spring Authorization 服務器的地址,該地址為 http://127.0.0.1:9000。 代碼挑戰方法參數配置為 S256。 這些配置用於準備 UserManager 實例,我們稍後使用它來調用授權服務器。 該應用程序有兩個端點——“/” 用於訪問應用程序的登錄頁面,以及 “callback” 端點,用於處理授權服務器的重定向請求:
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Login from './components/LoginHandler';
import CallbackHandler from './components/CallbackHandler';
import pkceAuthConfig from './pkceAuthConfig';
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';
function App() {
const [authenticated, setAuthenticated] = useState(null);
const [userInfo, setUserInfo] = useState(null);
const userManager = new UserManager({
userStore: new WebStorageStateStore({ store: window.localStorage }),
...pkceAuthConfig,
});
function doAuthorize() {
userManager.signinRedirect({state: '6c2a55953db34a86b876e9e40ac2a202',});
}
useEffect(() => {
userManager.getUser().then((user) => {
if (user) {
setAuthenticated(true);
}
else {
setAuthenticated(false);
}
});
}, [userManager]);
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Login authentication={authenticated} handleLoginRequest={doAuthorize}/>}/>
<Route path="/callback"
element={<CallbackHandler
authenticated={authenticated}
setAuth={setAuthenticated}
userManager={userManager}
userInfo={userInfo}
setUserInfo={setUserInfo}/>}/>
</Routes>
</BrowserRouter>
);
}
export default App;
5. 測試
我們將使用具有 OIDC 客户端支持啓用的 React 應用程序來測試流程。要安裝所需的依賴項,我們需要從應用程序的根目錄運行 <em >npm install</em> 命令。然後,我們將使用 <em >npm start</em> 命令啓動應用程序。
5.1. 通過授權碼流程訪問應用程序
本客户端應用程序執行以下兩個活動:首先,訪問主頁 http://127.0.0.1:3000 會渲染登錄頁面。 這是一個我們 SPA 應用程序的登錄頁面; 接下來,當我們進行登錄後,SPA 應用程序會使用 code_challenge 和 code_challenge_method 調用 Spring 授權服務器: 如所示,我們可以看到向 Spring 授權服務器發出的請求 http://127.0.0.1:9000,請求參數如下:
http://127.0.0.1:9000/oauth2/authorize?
client_id=public-client&
redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback&
response_type=code&
scope=openid+profile+email&
state=301b4ce8bdaf439990efd840bce1449b&
code_challenge=kjOAp0NLycB6pMChdB7nbL0oGG0IQ4664OwQYUegzF0&
code_challenge_method=S256&
response_mode=query授權服務器將請求重定向到 Spring Security 登錄頁面: 一旦我們提供登錄憑據,授權服務器就會請求對額外的 OAuth 範圍(profile 和 email)的同意。 這是由於授權服務器配置 為 true:
5.2. 交換授權碼獲取訪問令牌
如果完成登錄,授權服務器會返回授權碼。隨後,SPA 向授權服務器發送另一個 HTTP 請求,以獲取訪問令牌。SPA 將先前請求中獲得的授權碼以及 <em >code_challenge</em> 一起提供,從而獲取 <em >access_token</em>: 對於上述請求,Spring 授權服務器會返回訪問令牌:
接下來,我們訪問授權服務器的
<em >userinfo</em> 端點,以獲取用户詳情。我們使用 <em >access_token</em> 作為 Bearer 令牌,通過 Authorization HTTP 頭部訪問該端點。從 <em >userinfo</em> 詳情中打印的用户信息如下:
6. 結論
在本文中,我們演示瞭如何在單頁應用程序中使用 OAuth 2.0 PKCE 擴展,並結合 Spring Authorization Server。我們首先探討了 PKCE 對公共客户端的需求,並探索了在 Spring Authorization Server 中配置 PKCE 流。最後,我們利用 React 應用程序來演示該流程。