PKCE Support for Secret Clients with Spring Security – 用Spring Security为秘密客户端提供PKCE支持

最后修改: 2022年 8月 24日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this tutorial, we’ll show to use PKCE in a Spring Boot confidential client application.

在本教程中,我们将展示如何在Spring Boot保密客户端应用程序中使用PKCE。

2. Background

2. 背景

Proof Key for Code Exchange (PKCE) is an extension to the OAuth protocol that initially targeted public clients, usually SPA web applications or mobile apps. It is used as part of the Authorization Code Grant flow and helps to mitigate some attacks by a malicious third party.

代码交换的证明密钥(PKCE)是OAuth协议的一个扩展,最初针对的是公共客户,通常是SPA网络应用程序或移动应用程序。它被用作授权代码授予流程的一部分,有助于减轻恶意第三方的一些攻击

The main vector for those attacks is the step that happens when the provider has already established the user’s identity and sends the authorization code using an HTTP redirect. Depending on the scenario, this authorization code can leak and/or be intercepted, allowing the attacker to use it to obtain a valid access token.

这些攻击的主要载体是当供应商已经建立了用户的身份并使用HTTP重定向发送授权代码时发生的步骤。根据不同的情况,这个授权代码可能会泄漏和/或被拦截,允许攻击者使用它来获得一个有效的访问令牌。

Once in possession of this access token, the attacker can use it to access a protected resource and use it as if it was the legitimate owner. For example, if this access token is associated with a banking account, they can then access statements, portfolio values, or other sensitive information.

一旦掌握了这个访问令牌,攻击者就可以用它来访问受保护的资源,并把它当作合法所有者来使用。例如,如果这个访问令牌与银行账户有关,他们就可以访问报表、投资组合价值或其他敏感信息。

3. PKCE Modifications to OAuth

3.PKCE对OAuth的修改

The PKCE mechanism adds a few tweaks to the standard authorization code flow:

PKCE机制在标准授权代码流程中增加了一些调整。

  • The client sends two additional parameters in the initial authorization request: code_challenge and code_challenge_method
  • In the last step, when the client exchanges an authorization code for an access token, there’s also a new parameter: code_verifier

A PKCE-enabled client takes the following steps to implement this mechanism:

一个支持PKCE的客户端采取以下步骤来实现这一机制。

First, it generates a random string to use as the code_verifier parameter. According to RFC 7636, the length of this string must be at least 43 octets but less than 128 octets. The key point is to use a secure random generator, such as the JVM’s SecureRandom or equivalent.

首先,它生成一个随机字符串,作为code_verifier参数使用。根据RFC 7636,这个字符串的长度必须至少为43个八位字节,但小于128个八位字节。关键点是要使用安全的随机生成器,例如JVM的SecureRandom或同等的随机生成器。

Besides its length, there’s also a restriction on the range of allowed characters: only alphanumeric ASCII characters are supported, along with a few symbols.

除了长度,对允许的字符范围也有限制:只支持字母数字的ASCII字符,还有一些符号。

Next, the client takes the generated value and transforms it into the code_challenge parameter using a supported method. Currently, the specification mentions just two transformation methods: plain and S256.

接下来,客户端使用支持的方法将生成的值转换为code_challenge参数。目前,该规范说明只有两种转换方法。plainS256

  • plain is just a no-op transformation, so the transformed value is the same as the code_verifier
  • S256 corresponds to the SHA-256 hashing algorithm, whose result is encoded in BASE64

The client then builds the OAuth authorization URL using the regular parameters (client_id, scope, state, etc.) and adds the resulting code_challenge and code_challenge_method.

然后,客户端使用常规参数(client_id, scope, state等)建立OAuth授权URL,并添加由此产生的code_challengecode_challenge_method

3.1. Code Challenge Verification

3.1.代码挑战验证

In the last step of an OAuth authorization code flow, the client sends the original code_verifier value along with the regular ones as defined by this flow. The server then validates the code_verifier according to the challenge’s method:

在OAuth授权代码流程的最后一步,客户端将原始的code_verifier值与本流程所定义的常规值一起发送。然后,服务器根据挑战的方法来验证code_verifier

  • For the plain method, code_verifier and the challenge must be the same
  • For the S256 method, the server calculates the SHA-256 of the supplied value and encodes it in BASE64 before comparing it with the original challenge.

So, why is PKCE effective against authorization code attacks? As we mentioned before, those usually target the redirect sent from the authorization server, which contains the authorization code, to work. However, with PKCE, this information is no longer sufficient to complete the flow, at least for the S256 method. The code-for-token exchange only happens if the client provides both the authorization code and the verifier, which is never present in the redirects.

那么,为什么PKCE对授权码攻击有效?正如我们之前提到的,那些通常针对从授权服务器发送的重定向,其中包含了授权码,才能发挥作用。然而,有了PKCE,这些信息不再足以完成流程,至少对于S256方法来说是如此。只有在客户端同时提供授权码验证器的情况下,才会发生代码换令牌的交换,而这一点在重定向中从未出现。

Of course, when using the plain method, the verifier and challenge are the same, so there’s no point in using this method in real-world applications.

当然,当使用plain方法时,验证者和挑战者是一样的,所以在现实世界的应用中使用这种方法没有意义。

3.2. PKCE for Secret Clients

3.2.秘密客户端的PKCE

In OAuth 2.0, PKCE is optional and mostly used with mobile and web applications. The upcoming OAuth 2.1 version, however, made PKCE mandatory not only for public clients but also for secret ones.

在OAuth 2.0中,PKCE是可选的,主要用于移动和网络应用。然而,即将推出的OAuth 2.1版本,不仅对公共客户,而且对秘密客户也是强制性的PKCE。

Just to remember, a secret client is usually a hosted application running in a cloud or on-premises server. Such clients also use the authorization code flow, but since the final code exchange step happens between the backend and the authorization servers, the user agent (web or mobile) never “sees” the access token.

只是要记住,秘密客户端通常是运行在云端或企业内部服务器中的托管应用程序。此类客户端也使用授权代码流,但由于最后的代码交换步骤发生在后台和授权服务器之间,用户代理(网络或移动)永远不会 “看到 “访问令牌。

Other than that, the steps are exactly the same as in the public client case.

除此以外,其步骤与公共客户案例完全相同。

4. Spring Security Support for PKCE

4.对PKCE的Spring安全支持

As of Spring Security 5.7, PKCE is fully supported for both servlet and reactive flavored web applications. However, this feature is not enabled by default since not all identity providers support this extension yet. Spring Boot applications must use version 2.7 or above of the framework and rely on standard dependency management. This ensures the project picks the correct Spring Security version, along with its transitive dependencies.

从Spring Security 5.7开始,Servlet和反应式Web应用都完全支持PKCE。然而,由于并非所有的身份提供商都支持这一扩展,因此这一功能并没有默认启用。Spring Boot应用程序必须使用2.7或以上版本的框架,并依赖标准的依赖性管理。这可以确保项目选择正确的Spring Security版本,以及其横向依赖。

PKCE support lives in the spring-security-oauth2-client module. For a Spring Boot application, the easiest way to bring this dependency is using the corresponding starter module:

PKCE支持存在于spring-security-oauth2-client模块中。对于Spring Boot应用程序来说,最简单的方法是使用相应的启动模块来带来这种依赖性。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.7.2</version>
</dependency>

The latest versions of those dependencies can be downloaded from Maven Central.

这些依赖项的最新版本可以从Maven中心下载。

With the dependencies in place, we now need to customize the OAuth 2.0 login process to support PKCE. For reactive applications, this means adding a SecurityWebFilterChain bean that applies this setting:

随着依赖关系的建立,我们现在需要定制OAuth 2.0的登录过程以支持PKCE。对于反应式应用程序,这意味着添加一个SecurityWebFilterChain Bean来应用这个设置。

@Bean
public SecurityWebFilterChain pkceFilterChain(ServerHttpSecurity http,
  ServerOAuth2AuthorizationRequestResolver resolver) {
    http.authorizeExchange(r -> r.anyExchange().authenticated());
    http.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
    return http.build();
}

The key step is setting a custom ServerOAuth2AuthorizationRequestResolver in the login specification. Spring Security uses an implementation of this interface to build an OAuth authorization request for a given client registration.

关键步骤是在登录规范中设置一个自定义的ServerOAuth2AuthorizationRequestResolverSpring Security使用该接口的实现来为给定的客户端注册建立一个OAuth授权请求。

Fortunately, we don’t have to implement this interface. Instead, we can use the readily available DefaultServerOAuth2AuthorizationRequestResolver class, which allows us to apply further customizations:

幸运的是,我们不需要实现这个接口。相反,我们可以使用现成的DefaultServerOAuth2AuthorizationRequestResolver类,它允许我们应用进一步的定制。

@Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(ReactiveClientRegistrationRepository repo) {
    var resolver = new DefaultServerOAuth2AuthorizationRequestResolver(repo);
    resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
    return resolver;
}

Here, we instantiate the request resolver, passing a ReactiveClientRegistrationRepository instance. Then, we use OAuth2AuthorizationRequestCustomizers.withPkce(), which provides the required logic to add the additional PKCE parameters to the authorization request URL.

在这里,我们实例化请求解析器,传递一个ReactiveClientRegistrationRepository实例。然后,我们使用OAuth2AuthorizationRequestCustomizers.withPkce(),它提供了所需的逻辑,将额外的PKCE参数添加到授权请求URL中。

5. Testing

5.测试

To test our PKCE-enabled application, we need an authorization server that supports this extension. In this tutorial, we’ll use the Spring Authorization Server for this purpose. This project is a recent addition to Spring’s family that allows us to quickly build an OAuth 2.1/OIDC-compliant authorization server.

为了测试我们支持PKCE的应用程序,我们需要一个支持该扩展的授权服务器。在本教程中,我们将使用Spring授权服务器实现这一目的。该项目是Spring家族的最新成员,它允许我们快速构建一个符合OAuth 2.1/OIDC的授权服务器。

5.1. Authorization Server Setup

5.1.授权服务器的设置

In our live test environment, the authorization server runs as a separate process from the client. The project is a standard Spring Boot web application to which we’ve added the relevant maven dependency:

在我们的实时测试环境中,授权服务器作为一个独立的进程运行,与客户端分开。该项目是一个标准的Spring Boot网络应用,我们在其中加入了相关的maven依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.3.1</version>
</dependency>

The latest version of the starter and  Spring Authorization Server can be downloaded from Maven Central.

最新版本的starterSpring Authorization Server可以从Maven Central下载。

To work properly, the Authorization Server requires us to provide a few configuration beans, including a RegisteredClientRepository and an UserDetailsService. For our testing purposes, we can use in-memory implementations of both containing a fixed set of test values. For this tutorial, the former is more relevant:

为了正常工作,授权服务器要求我们提供一些配置Bean,包括一个RegisteredClientRepository和一个UserDetailsService。对于我们的测试目的,我们可以使用两者的内存实现,包含一组固定的测试值。对于本教程,前者更有意义。

@Bean 
public RegisteredClientRepository registeredClientRepository() {      
    var pkceClient = RegisteredClient
      .withId(UUID.randomUUID().toString())
      .clientId("pkce-client")
      .clientSecret("{noop}obscura")
      .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
      .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
      .scope(OidcScopes.OPENID)          
      .scope(OidcScopes.EMAIL)          
      .scope(OidcScopes.PROFILE)
      .clientSettings(ClientSettings.builder()
        .requireAuthorizationConsent(false)
        .requireProofKey(true)
        .build())
      .redirectUri("http://127.0.0.1:8080/login/oauth2/code/pkce")
      .build();
    
    return new InMemoryRegisteredClientRepository(pkceClient);
}

The key point is using the clientSettings() method to enforce the use of PKCE for a particular client. We do this by passing a ClientSettings object created with the requireProofKey() set to true.

关键点是使用clientSettings()方法为特定客户强制使用PKCE。我们通过传递一个ClientSettings对象来做到这一点,该对象是在requireProofKey()设置为true后创建的。

In our test setup, the client will run on the same host as the authorization server, so we’re using 127.0.0.1 as the hostname part of the redirect URL. It is worth noting that using “localhost” is not allowed here, hence the use of the equivalent IP address.

在我们的测试设置中,客户端将在与授权服务器相同的主机上运行,所以我们使用127.0.0.1作为重定向URL的主机名部分。值得注意的是,在这里使用 “localhost “是不允许的,因此要使用相应的IP地址。

To complete the setup, we’ll also need to modify the default port setting in the application’s properties file:

为了完成设置,我们还需要修改应用程序的属性文件中的默认端口设置。

server.port=8085

5.2. Running Live Tests

5.2.运行实时测试

Now, let’s run a live test to verify that all is working as intended. We can run both projects straight from the IDE or open two shell windows and issue the command mvn spring-boot:run for each module. Regardless of the method, once both applications are up, we can open a browser and point it to http://127.0.0.1:8080.

现在,让我们运行一个实时测试,以验证所有的工作是否按预期进行。我们可以直接从IDE中运行两个项目,或者打开两个shell窗口,为每个模块发出mvn spring-boot:run命令。无论采用哪种方法,一旦两个应用程序都启动了,我们就可以打开一个浏览器并将其指向http://127.0.0.1:8080

We should see Spring Security’s default login page:

我们应该看到Spring Security的默认登录页面。

pkce sign in

Notice the URL in the address bar: http://localhost:8085. This means that the login form came from the authorization server through a redirect. To verify this statement, we can open Chrome’s DevTools (or the equivalent in your browser of choice) while on the login form and reenter the initial URL in the address bar:

注意地址栏中的URL。http://localhost:8085这意味着登录表单是通过重定向来自授权服务器的。为了验证这一说法,我们可以在登录表单上打开Chrome的DevTools(或你选择的浏览器中的相应功能),并在地址栏中重新输入初始URL。

pkce challenge

We can see PKCE parameters in the Location header present in the response generated by our client application to the request made to http://127.0.0.1:8080/oauth2/authorization/pkce:

我们可以看到PKCE参数出现在客户端应用程序对http://127.0.0.1:8080/oauth2/authorization/pkce的请求所产生的响应中的位置头。

Location: http://localhost:8085/oauth2/authorize?
  response_type=code&
  client_id=pkce-client&
  scope=openid email&
  state=sUmww5GH14yatTwnv2V5Xs0rCCJ0vz0Sjyp4tK1tsdI=&
  redirect_uri=http://127.0.0.1:8080/login/oauth2/code/pkce&
  nonce=FVO5cA3_UNVVIjYnZ9ZrNq5xCTfDnlPERAvPCm0w0ek&
  code_challenge=g0bA5_PNDxy-bdf2t9H0ximVovLqMdbuTVxmGnXjdnQ&
  code_challenge_method=S256

To complete the login sequence, we’ll use “user” and “password” as credentials. If we continue to follow the requests, we’ll see that neither the code verifier nor the access token is ever present, which was our goal.

为了完成登录序列,我们将使用 “用户 “和 “密码 “作为凭证。如果我们继续跟踪这些请求,我们会看到代码验证器和访问令牌都没有出现过,这就是我们的目标。

6. Conclusion

6.结语

In this tutorial, we’ve shown how to enable OAuth’s PKCE extension in a Spring Security application with just a few lines of code. Furthermore, we’ve also shown how to use the Spring Authorization Server library to create a tailor-made server for testing purposes.

在本教程中,我们展示了如何仅用几行代码就在Spring Security应用程序中启用OAuth的PKCE扩展。此外,我们还展示了如何使用Spring授权服务器库来创建一个量身定做的服务器用于测试。

As usual, all code is available over on GitHub.

像往常一样,所有的代码都可以在GitHub上找到