Spring Security 6中使用PKCE實現身份驗證

banq發表於2024-06-07

在本教程中,我們將討論OAuth 2.0公共客戶端的程式碼交換證明金鑰 (PKCE) 的使用。

背景
OAuth 2.0 公共客戶端(例如單頁應用程式 (SPA) 或使用授權碼授予的移動應用程式)容易受到授權碼攔截攻擊。如果客戶端-伺服器通訊發生在不安全的網路上,惡意攻擊者可能會從授權端點攔截授權碼。

如果攻擊者可以訪問授權程式碼,則可以使用它來獲取訪問令牌。一旦攻擊者擁有訪問令牌,它就可以像合法應用程式使用者一樣訪問受保護的應用程式資源,從而嚴重危害應用程式。例如,如果訪問令牌與金融應用程式相關聯,攻擊者可能會訪問敏感的應用程式資訊。

OAuth 程式碼攔截攻擊
在本節中,讓我們討論一下 Oauth 授權程式碼攔截攻擊是如何發生的:  

惡意攻擊者如何濫用授權授予程式碼來獲取訪問令牌的流程:

  • 合法的 OAuth 應用程式使用其 Web 瀏覽器啟動 OAuth 授權請求流程,並提供所有必需的詳細資訊
  • Web 瀏覽器向授權伺服器傳送請求
  • 授權伺服器將授權碼返回給網頁瀏覽器
  • 在此階段,如果通訊透過不安全的渠道進行,惡意使用者可能會訪問授權碼
  • 惡意使用者交換授權碼授權以從授權伺服器獲取訪問令牌
  • 由於授權許可有效,授權伺服器向惡意應用程式發出訪問令牌。惡意應用程式可以濫用訪問令牌,以合法應用程式的名義訪問受保護的資源
  • 程式碼交換的證明金鑰是 OAuth 框架的擴充套件,旨在減輕這種攻擊。

使用 OAuth 的 PKCE
PKCE 擴充套件包括 OAuth 授權碼授予流程的以下附加步驟:

  • 客戶端應用程式在初始授權請求中傳送兩個附加引數code_challenge和code_challenge_method
  • 客戶端在下一步交換授權碼以獲取訪問令牌時,還會傳送code_verifier
  • 首先,支援 PKCE 的客戶端應用程式會選擇一個動態建立的加密隨機金鑰,稱為code_verifier。此code_verifier對於每個授權請求都是唯一的。根據PKCE 規範, code_verifier值的長度必須介於 43 到 128 個八位位元組之間。

此外,code_verifier只能包含字母數字ASCII 字元和一些允許的符號。其次,使用支援的code_challenge_method將code_verifier轉換為code_challenge。目前,支援的轉換方法是plain和S256。plain是一種無操作轉換,使code_challenge值與code_verifier保持一致。S256 方法首先生成 code_verifier 的 SHA-256 雜湊,然後對雜湊值執行 Base64 編碼。

防止OAuth程式碼攔截攻擊
下面演示了 PKCE 擴充套件如何防止訪問令牌被盜:

  • 合法的 OAuth 應用程式使用其 Web 瀏覽器啟動 OAuth 授權請求流程,提供所有必需的詳細資訊以及code_challenge和code_challenge_method引數。
  • Web 瀏覽器將請求傳送到授權伺服器,併為客戶端應用程式儲存code_challenge和code_challenge_method 
  • 授權伺服器將授權碼返回給網頁瀏覽器
  • 在此階段,如果通訊透過不安全的渠道進行,惡意使用者可能會訪問授權碼
  • 惡意使用者嘗試交換授權程式碼授權,以從授權伺服器獲取訪問令牌。但是,惡意使用者不知道需要隨請求一起傳送的code_verifier 。授權伺服器拒絕向惡意應用程式傳送訪問令牌請求
  • 合法應用程式提供code_verifier和授權許可以獲得訪問令牌。授權伺服器根據提供的code_verifier和授權程式碼授予請求中先前儲存的code_challenge_method計算code_challenge 。它將計算出的code_challenge與先前儲存的code_challenge進行匹配。這些值始終匹配,並且客戶端會獲得訪問令牌
  • 客戶端可以使用此訪問令牌訪問應用程式資源

使用 Spring Security 的 PKCE
從 6.3 版開始,Spring Security 支援 servlet 和響應式 Web 應用程式的 PKCE。但是,預設情況下不啟用它,因為並非所有身份提供者都支援 PKCE 擴充套件。當客戶端在不受信任的環境(例如本機應用程式或基於 Web 瀏覽器的應用程式)中執行,並且client_secret為空或未提供,並且客戶端身份驗證方法設定為none 時,會自動為公共客戶端使用 PKCE 。

1. Maven 配置
Spring 授權伺服器支援 PKCE 擴充套件。因此,在 Spring 授權伺服器應用程式中包含 PKCE 支援的簡單方法是包含spring-boot-starter-oauth2-authorization-server依賴項:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    <version>3.3.0</version>
</dependency>

2. 註冊公共客戶端
接下來,讓我們透過在application.yml檔案中配置以下屬性來註冊一個公共的單頁應用程式客戶端:

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          public-client:
            registration:
              client-id: <font>"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要求終端使用者在成功認證後提供額外的同意才能訪問個人資料和電子郵件範圍。re ​​quire-proof-key配置可防止 PKCE 降級攻擊。

啟用require-proof-key配置後,授權伺服器將不允許任何惡意嘗試繞過沒有 code_challenge 的 PKCE 流程。其餘配置是向授權伺服器註冊客戶端的標準配置。

3. Spring Security 配置
接下來,讓我們為授權伺服器定義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(<font>"/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
      .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.cors(Customizer.withDefaults())
      .build();
}

在上面的配置中,我們首先應用授權服​​務器的預設安全設定。然後,我們應用 OIDC、CORS 和 Oauth2 資源伺服器的 Spring 安全預設設定。現在讓我們定義另一個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(<font>"*");
    config.addAllowedMethod(
"*");
    config.addAllowedOrigin(
"http://127.0.0.1:3000");
    config.setAllowCredentials(true);
    source.registerCorsConfiguration(
"/**", config);
    return source;
}

我們正在定義一個CorsConfigurationSource例項,其中包含允許的來源、標頭、方法和其他配置。請注意,在上面的配置中,我們使用 IP 地址 127.0.0.1 而不是localhost,因為後者是不允許的。最後,讓我們定義一個UserDetailsS ​​ervice 例項來在授權伺服器中定義使用者。

@Bean
UserDetailsService userDetailsService() {
    PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    UserDetails userDetails = User.builder()
      .username(<font>"john")
      .password(
"password")
      .passwordEncoder(passwordEncoder::encode)
      .roles(
"USER")
      .build();
    return new InMemoryUserDetailsManager(userDetails);
}

透過以上配置,我們將能夠使用使用者名稱john和password作為密碼來向授權伺服器進行身份驗證。

4. 公共客戶端應用程式
現在讓我們討論一下公共客戶端。為了演示目的,我們使用一個簡單的 React 應用程式作為單頁應用程式。此應用程式使用oidc-client-ts庫來提供客戶端 OIDC 和 OAuth2 支援。SPA 應用程式配置了以下配置:

const pkceAuthConfig = {
  authority: 'http:<font>//127.0.0.1:9000/',<i>
  client_id: 'public-client',
  redirect_uri: 'http:
//127.0.0.1:3000/callback',<i>
  response_type: 'code',
  scope: 'openid profile email',
  post_logout_redirect_uri: 'http:
//127.0.0.1:3000/',<i>
  userinfo_endpoint: 'http:
//127.0.0.1:9000/userinfo',<i>
  response_mode: 'query',
  code_challenge_method: 'S256',
};
export default pkceAuthConfig;

授權配置了 Spring 授權伺服器的地址,即http://127.0.0.1:9000。程式碼質詢方法引數配置為 S256。這些配置用於準備 UserManager例項,稍後我們將使用它來呼叫授權伺服器。此應用程式有兩個端點 - “/” 用於訪問應用程式的登入頁面,以及處理來自授權伺服器的回撥請求的“回撥”端點:

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=<font>"/" 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;

測試
我們將使用啟用了 OIDC 客戶端支援的 React 應用程式來測試流程。要安裝所需的依賴項,我們需要從應用程式的根目錄執行npm install命令。然後,我們將使用npm start命令啟動該應用程式。

1. 訪問授權碼授予申請
此客戶端應用程式執行以下兩個活動:首先,訪問http://127.0.0.1:3000上的主頁會呈現登入頁面。這是我們的 SPA 應用程式的登入頁面:接下來,一旦我們繼續登入,SPA 應用程式就會使用 code_challenge和code_challenge_method呼叫Spring 授權伺服器:我們可以注意到對http://127.0.0.1:9000上的 Spring 授權伺服器發出的請求具有以下引數:

http:<font>//127.0.0.1:9000/oauth2/authorize?<i>
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 範圍配置檔案和電子郵件。這是由於授權伺服器中的配置require-authorization-consent為 true。

2. 使用授權碼兌換訪問令牌

  • 如果我們完成登入,授權伺服器將返回授權碼。 隨後,SPA 向授權伺服器請求另一個 HTTP 以獲取訪問令牌。 SPA提供上一個請求中獲得的授權碼以及code_challenge以獲取access_token
  • 對於上述請求,Spring 授權伺服器使用訪問令牌進行響應
  • 接下來,我們訪問授權伺服器中的userinfo端點以訪問使用者詳細資訊。 我們提供帶有 Authorization HTTP 標頭的access_token作為 Bearer 令牌來訪問此端點。 此使用者資訊從userinfo詳細資訊中列印出來:

相關文章