为什么 AccessTokenRequest 的 PreservedState 永远为 null,并导致与 CSRF 相关的 InvalidRequestException?

Why is AccessTokenRequest's PreservedState perpetually null with a resultant CSRF related InvalidRequestException?

作为上下文,我一直在尝试获得一个相当简单的@SprintBootApplication,它带有一个额外的@EnableOAuth2Sso 注释,与 WSO2 Identity Server 集成已经有一段时间了。

在我看来,让这项工作正常进行应该是配置问题(如 Spring Cloud Security 上所宣传的那样)- 但到目前为止我运气不好。

为了了解发生了什么,我使用我的调试器逐句通过 spring-security-oauth2 代码来弄清楚发生了什么。在这样做的过程中,我注意到我的 AccessTokenRequest 的 PreservedState 永远为 null,并产生与 CSRF 相关的 InvalidRequestException。这是相关代码:

public class AuthorizationCodeAccessTokenProvider extends OAuth2AccessTokenSupport implements AccessTokenProvider {

....

private MultiValueMap<String, String> getParametersForTokenRequest(AuthorizationCodeResourceDetails resource,
        AccessTokenRequest request) {

    MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
    form.set("grant_type", "authorization_code");
    form.set("code", request.getAuthorizationCode());

    Object preservedState = request.getPreservedState();
    if (request.getStateKey() != null || stateMandatory) {
        // The token endpoint has no use for the state so we don't send it back, but we are using it
        // for CSRF detection client side...
        if (preservedState == null) {
            throw new InvalidRequestException(
                    "Possible CSRF detected - state parameter was required but no state could be found");
        }
    }

当谈到上面的代码时,我在使用 admin/admin 登录并且我的网络应用程序收到了授权代码后,我已经批准了声明:

http://localhost:9998/loginstate=Uu8ril&code=20ffbb6e4107ce3c5cf9ee22065f4f2 

考虑到我首先需要做的就是让登录部分正常工作,我尝试禁用 CSRF,但无济于事。

相关配置如下:

spring:
  profiles: default
security:
  oauth2:
    client:
      accessTokenUri: https://localhost:9443/oauth2/token
      userAuthorizationUri: https://localhost:9443/oauth2/authorize
      clientId: yKSD9XwET9XJ3srGEFXP6AfHhAka
      clientSecret: zuPTcdJH435h3wgl055XNZ5ffNMa
      scope: openid
      clientAuthenticationScheme: header
     resource:
      userInfoUri: https://localhost:9443/oauth2/userinfo?schema=openid

就我自己的调查工作而言,令人担忧的是 DefaultOAuthClientContext preserved 状态在需要使用之前被清除,这似乎是一个顺序问题。

我正在使用最新版本的 Spring Boot (1.3.0) 和 WSO2 Identity Server (5.0)。同时使用 spring-security-oauth 2.0.8.

事实证明,在提供的代码部分中引用的 preservedState 为 null 的原因是因为正在创建 bean Oauth2ClientContext 的新实例,这正是不应该发生的事情 - OAuth2ClientContext 的全部目的是存储状态。就 OAuth2 协议 (RFC 6749) 而言,保存状态对于防止跨站点请求伪造很重要(请参阅第 10.12 节)。

选择它是一件简单的事情,只需启用调试日志记录,并将针对 WSO2 IS 生成的输出与在工作示例中看到的输出进行比较。在我的例子中,我总是返回的工作示例是由 Spring 团队自己提供的。

这是客户端配置(application.yml)然后使用Spring团队SSO服务器进行日志输出测试:

spring:
  profiles: default
security:
  oauth2:
    client:
      accessTokenUri: http://192.168.0.113:32768/uaa/oauth/token
      userAuthorizationUri: http://192.168.0.113:32768/uaa/oauth/authorize
  clientId: acme
  clientSecret: acmesecret
resource:
  jwt:
    keyValue: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGp/Q5lh0P8nPL21oMMrt2RrkT9AW5jgYwLfSUnJVc9G6uR3cXRRDCjHqWU5WYwivcF180A6CWp/ireQFFBNowgc5XaA0kPpzEtgsA5YsNX7iSnUibB004iBTfU9hZ2Rbsc8cWqynT0RyN4TP1RYVSeVKvMQk4GT1r7JCEC+TNu1ELmbNwMQyzKjsfBXyIOCFU/E94ktvsTZUHF4Oq44DBylCDsS1k7/sfZC2G5EU7Oz0mhG8+Uz6MSEQHtoIi6mc8u64Rwi3Z3tscuWG2ShtsUFuNSAFNkY7LkLn+/hxLCu2bNISMaESa8dG22CIMuIeRLVcAmEWEWH5EEforTg+QIDAQAB
      -----END PUBLIC KEY-----
  id: openid
  serviceId: ${PREFIX:}resource

请注意,没有一行提到创建 OAuth2ClientContext.

DEBUG o.s.security.web.FilterChainProxy - /login?code=9HLSpP&state=G9kpy3 at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/login'; against '/login'
DEBUG o.s.s.o.c.f.OAuth2ClientAuthenticationProcessingFilter - Request is to process authentication
INFO  o.s.s.o.c.DefaultOAuth2ClientContext - Getting access token request
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.accessTokenRequest'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.accessTokenRequest'
INFO  o.s.s.o.client.OAuth2RestTemplate - Longer lived state key: G9kpy3
INFO  o.s.s.o.client.OAuth2RestTemplate - Removing preserved state in oauth2context
INFO  o.s.s.o.c.DefaultOAuth2ClientContext - Found preserved state: http://localhost:9999/login

这是客户端配置(application.yml)然后使用WSO2IS 5.0.0进行日志输出测试:

spring:
  profiles: wso2
server:
  port: 9998
security:
  oauth2:
    client:
      accessTokenUri: https://localhost:9443/oauth2/token
      userAuthorizationUri: https://localhost:9443/oauth2/authorize
      clientId: yKSD9XwET9XJ3srGEFXP6AfHhAka
      clientSecret: zuPTcdJH435h3wgl055XNZ5ffNMa
      scope: openid
      clientAuthenticationScheme: header
    resource:
      userInfoUri: https://localhost:9443/oauth2/userinfo?schema=openid

记下 Creating instance of bean 'scopedTarget.oauth2ClientContext'.

DEBUG o.s.security.web.FilterChainProxy - /login?state=PWhQwv&code=372ff0c197a4c85a0caf070cc9a6678 at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/login'; against '/login'
DEBUG o.s.s.o.c.f.OAuth2ClientAuthenticationProcessingFilter - Request is to process authentication
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.oauth2ClientContext'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2RestOperationsConfiguration$SessionScopedConfiguration$ClientContextConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.oauth2ClientContext'
INFO  o.s.s.o.c.DefaultOAuth2ClientContext - Getting access token request
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.accessTokenRequest'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.accessTokenRequest'
INFO  o.s.s.o.client.OAuth2RestTemplate - Longer lived state key: PWhQwv
INFO  o.s.s.o.client.OAuth2RestTemplate - Removing preserved state in oauth2context
INFO  o.s.s.o.c.DefaultOAuth2ClientContext - Found preserved state: null

最后,下一个停靠点自然是确定为什么没有使用 WSO2 IS 配置创建 OAuth2ClientContext。调查表明,这是因为 WSO2 IS 没有传回预期的 JSESSIONID,因此将找不到会话范围的 OAuth2ClientContext。

如果绝望,解决这种情况的潜在黑客是克隆 Spring OAuth 2 并执行以下操作:

在 class AuthorizationCodeAccessTokenProvider 中执行以下操作,更改请求中的保留状态。

    private MultiValueMap<String, String>     getParametersForTokenRequest(AuthorizationCodeResourceDetails resource,
        AccessTokenRequest request) {

            MultiValueMap<String, String> form = new   LinkedMultiValueMap<String, String>();
            form.set("grant_type", "authorization_code");
            form.set("code", request.getAuthorizationCode());

            request.setPreservedState("http://localhost:9998/login");
            Object preservedState = request.getPreservedState();

刚刚在试用时遇到了类似的问题https://spring.io/guides/tutorials/spring-boot-oauth2/ 并观察到此问题与 chrome 浏览器有关,它在 facebook 重定向后未传递 JSESSIONID。

当我使用 Safari 时同样有效,(只是将其添加到此处以节省某人的两个小时!)

你可以看到 set-cookie 被调用了两次 login/facebook 调用没有传递之前设置的 sessionid