/oauth/token 中的 XSRF 令牌无效

Posted

技术标签:

【中文标题】/oauth/token 中的 XSRF 令牌无效【英文标题】:Invalid XSRF token at /oauth/token 【发布时间】:2016-08-31 21:31:03 【问题描述】:

多重身份验证的 Spring OAuth2 实现的完整代码已上传到a file sharing site at this link。以下说明可在几分钟内在任何计算机上重现当前问题。


**当前问题:**
大多数身份验证算法都能正常工作。直到下面显示的控制流结束,程序才会中断。具体来说,在下面的 **SECOND PASS** 结束时会抛出“Invalid CSRF token found for http://localhost:9999/uaa/oauth/token”错误。上面链接中的应用程序是通过在此Spring Boot OAuth2 GitHub sample 的`authserver` app 中添加自定义的`OAuth2RequestFactory`、`TwoFactorAuthenticationFilter` 和`TwoFactorAuthenticationController` 开发的。 **需要对以下代码进行哪些具体更改才能解决此 CSRF 令牌错误并启用 2 因素身份验证?** 我的研究使我怀疑 `CustomOAuth2RequestFactory` (API at this link) 可能是配置解决方案的地方,因为它定义了管理 `AuthorizationRequest`s 和 `TokenRequest`s 的方法。 **This section of the official OAuth2 spec表示向授权端点发出的请求的`state`参数是添加`csrf`令牌的地方。** 另外,链接中的代码使用the Authorization Code Grant Type described at this link to the official spec,这意味着流程中的步骤C没有更新`csrf`代码,从而触发步骤D中的错误。(您可以查看包括步骤C和步骤的整个流程D 在the official spec。)
**围绕当前错误的控制流:**
在下面的流程图中,通过 `TwoFactorAuthenticationFilter` 在 **SECOND PASS** 期间引发了当前错误。一切都按预期工作,直到控制流进入**第二次通过**。 以下流程图说明了可下载应用程序中的代码所采用的两因素身份验证过程的控制流程。 具体来说,`POST`s 和 `GET`s 序列的 Firefox `HTTP` 标头表明,序列中的每个请求都会发送相同的 `XSRF` cookie。 `XSRF` 令牌值在`POST /secure/two_factor_authentication` 之后才会引起问题,这会在`/oauth/authorize` 和`/oauth/token` 端点触发服务器处理,使用`/oauth/token`抛出 `Invalid CSRF token found for http://localhost:9999/uaa/oauth/token` 错误。 要了解上述控制流程图与 `/oauth/authorize` 和 `/oauth/token` 端点之间的关系,您可以在单独的浏览器窗口中并排比较上述流程图with the chart for the single factor flow at the official spec。上面的 **SECOND PASS** 只是第二次执行单因素官方规范中的步骤,但在 **SECOND PASS** 期间具有更大的权限。
**什么日志说:**
HTTP 请求和响应标头表明: 1.) 使用正确的 `username` 和 `password` 提交到 `9999/login` 会导致重定向到 `9999/authorize?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v` 后跟 `GET 9999/安全/two_factor_authenticated`。一个 XSRF 令牌在这些交易所中保持不变。 2.) 使用正确的 pin 码发送到 `9999/secure/two_factor_authentication` 的 POST 发送相同的 `XSRF` 令牌,并成功重定向到 `POST 9999/oauth/authorize` 并使其成为 `TwoFactorAuthenticationFilter.doFilterInternal( )` 并继续执行 `request 9999/oauth/token`,但 `9999/oauth/token` 拒绝该请求,因为相同的旧 XSRF 令牌与新的 `XSRF` 令牌值不匹配,该值显然是在 **第一次通过**。 `1.)` 和 `2.)` 之间的一个明显区别是 `2.)` 中的第二个 `request 9999/oauth/authorize` 不包含对 `9999/ 的第一个请求中包含的 url 参数在 `1.)` 中授权?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v`,也在the official spec 中定义。但目前尚不清楚这是否会导致问题。 此外,尚不清楚如何访问参数以从“TwoFactorAuthenticationController.POST”发送完整的请求。我为 `POST 9999/secure/two_factor_authentication` 控制器方法的`HttpServletRequest` 中的`parameters` `Map` 做了一个SYSO,它只包含`pinVal` 和`_csrf` 变量。 您可以在文件共享站点by clicking on this link 上阅读所有 HTTP 标头和 Spring Boot 日志。
**失败的方法:**
我尝试了@RobWinch's approach to a similar problem in the Spring Security 3.2 environment,但该方法似乎不适用于 Spring OAuth2 的上下文。具体来说,当在下面显示的 `TwoFactorAuthenticationFilter` 代码中取消注释以下 `XSRF` 更新代码块时,下游请求标头确实会显示不同/新的 `XSRF` 令牌值,但会引发相同的错误。 if(AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) CsrfToken token = (CsrfToken) request.getAttribute("_csrf"); response.setHeader("XSRF-TOKEN"/*"X-CSRF-TOKEN"*/, token.getToken()); **这表明需要更新`XSRF`配置以使`/oauth/authorize`和`/oauth/token`能够相互通信以及与客户端和资源应用程序通信以成功管理` XSRF` 令牌值。** 也许需要更改 `CustomOAuth2RequestFactory` 才能完成此操作。但是怎么做?
**相关代码:**
`CustomOAuth2RequestFactory` 的代码是: 公共类 CustomOAuth2RequestFactory 扩展 DefaultOAuth2RequestFactory 公共静态最终字符串 SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest"; 公共 CustomOAuth2RequestFactory(ClientDetailsS​​ervice clientDetailsS​​ervice) 超级(clientDetailsS​​ervice); @Override public AuthorizationRequest createAuthorizationRequest(Map authorizationParameters) ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpSession 会话 = attr.getRequest().getSession(false); 如果(会话!= null) AuthorizationRequest 授权请求 = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME); 如果(授权请求!= null) session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME); 返回授权请求; 返回 super.createAuthorizationRequest(authorizationParameters); `TwoFactorAuthenticationFilter` 的代码是: //这个类是根据:https://***.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2添加的 /** * 将 oauth 授权请求存储在会话中,以便它可以 * 稍后由 @link com.example.CustomOAuth2RequestFactory 选择 * 继续授权流程。 */ 公共类 TwoFactorAuthenticationFilter 扩展 OncePerRequestFilter 私有重定向策略重定向策略=新的默认重定向策略(); 私有 OAuth2RequestFactory oAuth2RequestFactory; //下面这两个是作为测试添加的,以避免在未定义时发生的编译错误。 公共静态最终字符串 ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED"; 公共静态最终字符串 ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED"; @自动连线 公共无效 setClientDetailsS​​ervice(ClientDetailsS​​ervice clientDetailsS​​ervice) oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsS​​ervice); 私人布尔twoFactorAuthenticationEnabled(收集机构) System.out.println(">>>>>>>>>>> 权限列表包括:"); 对于(GrantedAuthority 权威:权威) System.out.println("auth: "+authority.getAuthority() ); 返回 authority.stream().anyMatch( 权限-> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority()) ); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException System.out.println("------------------- INSIDE TwoFactorAuthenticationFilter.doFilterInternal() ------------------- -----"); // 检查用户是否没有进行两因素认证。 if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) System.out.println("++++++++++++++++++++++++++++认证但不是两个因素++++++++++++++ +++++++++++"); AuthorizationRequest 授权请求 = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request)); /* 检查客户端的权限 (authorizationRequest.getAuthorities()) 还是用户的权限 需要两个因素验证。 */ System.out.println("========================= twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) 是:" + twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities() )); System.out.println("========================= twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) 是:" + twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())); if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) || twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) // 将授权请求保存在会话中。这允许 CustomOAuth2RequestFactory // 用户成功后将此保存的请求返回到 AuthenticationEndpoint // 进行了两因素身份验证。 request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest); // 重定向用户需要输入二重验证码的页面 redirectStrategy.sendRedirect(请求,响应, ServletUriComponentsBuilder.fromCurrentContextPath() .path(TwoFactorAuthenticationController.PATH) .toUriString()); 返回; //下一个“IF”块在未注释时不能解决错误 //if(AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) // CsrfToken token = (CsrfToken) request.getAttribute("_csrf"); // 这是要作为标头或 HTTP 参数包含的令牌的值 // response.setHeader("XSRF-TOKEN", token.getToken()); // filterChain.doFilter(request, response); 私人地图 paramsFromRequest(HttpServletRequest 请求) 映射参数 = new HashMap(); for(入口条目:request.getParameterMap().entrySet()) params.put(entry.getKey(), entry.getValue()[0]); 返回参数;
**在您的计算机上重现问题:**
按照以下简单步骤,您只需几分钟即可在任何计算机上重现问题: 1.) 下载zipped version of the app from a file sharing site by clicking on this link。 2.) 输入解压应用程序:`tar -zxvf oauth2.tar(2).gz` 3.) 通过导航到 `oauth2/authserver` 然后输入 `mvn spring-boot:run` 来启动 `authserver` 应用程序。 4.) 通过导航到 `oauth2/resource` 然后输入 `mvn spring-boot:run` 来启动`resource` 应用程序 5.) 通过导航到 `oauth2/ui` 然后输入 `mvn spring-boot:run` 来启动 `ui` 应用程序 6.) 打开网络浏览器并导航到 `http://localhost:8080` 7.) 点击“登录”,然后输入“Frodo”作为用户,输入“MyRing”作为密码,点击提交。 8.) 输入“5309”作为“Pin Code”并点击提交。 **这将触发上面显示的错误。** 您可以通过以下方式查看完整的源代码: a.) 将 maven 项目导入您的 IDE,或通过 b.) 在解压缩的目录中导航并使用文本编辑器打开。
您可以在文件共享站点by clicking on this link 上阅读所有 HTTP 标头和 Spring Boot 日志。

【问题讨论】:

我发现在 /oauth/token 请求期间,请求缺少 csrf cookie,因此请求被 csrf 过滤器中止。因此显示错误。 @Md.MinhajurRahman 非常感谢。我今天将对此进行调查。你建议我如何处理你分享的信息? 我花了几个小时来找出确切的原因并尝试通过几种方式解决它,但最后我陷入了最后一个阶段,我发现我的情况分享给你。我对解决方案感兴趣。如果它仍然固定,请分享它。 @Md.MinhajurRahman 如果在添加CustomOAuth2RequestFactoryoauth/token 请求确实不包含csrf cookie,则您正在描述Spring OAuth2 中的错误。如果有错误,我们可以将其作为错误报告发布在 Spring OAuth2 GitHub 站点上。我正在独立分解他们的 API,试图了解它是如何工作的。但是,您是否愿意在可重现的步骤下方发布您发现的问题的答案,包括 Spring Boot 日志和记录您发现的问题的浏览器请求/响应标头? 我也面临同样的挑战;让 MFA 与 OAuth2 和 Spring Boot 一起工作。您能否在某处重新共享您的功能解决方案?现有份额不再有效。 【参考方案1】:

一个想法突然出现在我的脑海中:

如果激活会话固定,则在用户成功验证后创建一个新会话(请参阅 SessionFixationProtectionStrategy)。如果您使用默认的 HttpSessionCsrfTokenRepository,这当然也会创建一个新的 csrf 令牌。由于您提到了 XSRF-TOKEN 标头,我假设您使用了一些 javascript 前端。我可以想象用于登录的原始 csrf 令牌被存储并在之后重复使用 - 这将不起作用,因为这个 csrf 令牌不再有效。

您可以尝试禁用会话固定(http.sessionManagement().sessionFixation().none()<session-management session-fixation-protection="none"/>)或在登录后重新获取当前的 CSRF 令牌。

【讨论】:

http.sessionManagement().sessionFixation().none() 没有解决问题。虽然我愿意在登录后重新获取新的 CSRF 令牌,但我更喜欢与前端无关的东西。例如,authserver 应用程序的前端现在在 FreeMarker 中,ui 应用程序前端在 AngularJS 中。 @DollarCoffee 的方法承诺按照您的建议做,但从后端以与前端无关的方式。我只是不知道如何实现它,所以我正在研究。仅供参考,我将http.sessionManagement().sessionFixation().none().and() 放在http 之后的第一行【参考方案2】:

您的CustomOAuth2RequestFactory 将之前的request 替换为当前的request。但是,当您进行此切换时,您不会更新旧 request 中的 XSRF 令牌。以下是我对更新后的CustomOAuth2Request 的建议:

@Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) 
    ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
    HttpSession session = attr.getRequest().getSession(false);
    if (session != null) 
        AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
        if (authorizationRequest != null) 
            session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//UPDATE THE STATE VARIABLE WITH THE NEW TOKEN.  THIS PART IS NEW
            CsrfToken csrf = (CsrfToken) attr.getRequest().getAttribute(CsrfToken.class.getName());
            String attrToken = csrf.getToken();
            authorizationRequest.setState(attrToken);                

            return authorizationRequest;
        
    

    return super.createAuthorizationRequest(authorizationParameters);

我正在重新讨论这个问题,因为我最初的答案草稿被否决了。这个版本走的更远,我认为这是正确的方法。

【讨论】:

这个答案让我开始采用一种有望导致服务器端解决方案的方法。我奖励这个赏金,即使它有问题。例如,您为attr.getAttribute("_csrf", 1) 中的范围参数提供的值1 会为csrf 生成null 值。真正的scope 应该是引用当前会话的静态属性。并且可能还有其他问题。但这始于一个有希望的方向。我会报告更多结果。

以上是关于/oauth/token 中的 XSRF 令牌无效的主要内容,如果未能解决你的问题,请参考以下文章

401 无效令牌/handler.xsrf_token 与 cookie 不匹配

无法加载 http://localhost:9999/auth-service/oauth/token:预检响应具有无效的 HTTP 状态代码 401

从Spring Security Oauth2中的Token Store检索访问和刷新令牌的方法

Spring Security OAuth 2:如何在 oauth/token 请求后获取访问令牌和附加数据

Spring oauth2 / oauth / token无效凭据

如何刷新oAuth2令牌+弹簧安全