Spring Security /oauth/token处的XSRF令牌无效

gfttwv5a  于 11个月前  发布在  Spring
关注(0)|答案(2)|浏览(190)

当前问题:

大多数身份验证算法都能正常工作。程序直到下面所示的控制流结束时才会中断。具体来说,下面的SECOND PASS结束时会抛出Invalid CSRF token found for http://localhost:9999/uaa/oauth/token错误。上面链接中的应用程序是通过添加自定义OAuth2RequestFactory开发的,TwoFactorAuthenticationFilterTwoFactorAuthenticationController添加到此Spring Boot OAuth2 GitHub sampleauthserver app需要对以下代码进行哪些特定更改才能解决此CSRF令牌错误并启用双因素身份验证?
我的研究使我怀疑CustomOAuth2RequestFactoryAPI at this link)可能是配置解决方案的地方,因为它定义了管理AuthorizationRequestTokenRequest的方法。

OAuth2官方规范的这一部分指出,向授权端点发出的请求的state参数是添加csrf令牌的地方。

此外,链接中的代码使用了官方规范中的授权代码授予类型,这意味着流程中的步骤C不会更新csrf代码,从而触发了步骤D中的错误。(您可以在官方规范中查看包括步骤C和步骤D的整个流程。)

当前错误周围的控制流:

在下面的流程图中,当前错误是在通过TwoFactorAuthenticationFilterSECOND PASS期间抛出的。在控制流进入SECOND PASS之前,一切都按预期工作。
以下流程图说明了可下载应用程序中的代码所采用的双因素身份验证过程的控制流。
x1c 0d1x的数据
具体来说,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端点之间的关系,您可以在单独的浏览器窗口中将上述流程图与官方规格中的单因素流程图进行并排比较。上面的Second Pass只是将单因素官方规格中的步骤再次运行一次,但在第二次通过期间具有更大的权限。

日志内容:

HTTP请求和响应头表明:
1.)使用正确的usernamepassword提交到9999/login的POST会导致重定向到9999/authorize?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v,然后是GET 9999/secure/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不包含1.)中第一个请求9999/authorize?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v中包含的url参数,并且在官方规范中也定义了。但不清楚这是否导致了问题。
此外,不清楚如何访问参数以从TwoFactorAuthenticationController.POST发送完全格式的请求。我在POST 9999/secure/two_factor_authentication控制器方法的HttpServletRequest中对parametersMap进行了SYSO,它包含的所有变量都是pinVal_csrf
您可以在文件共享站点by clicking on this link上读取所有HTTP Header和Sping Boot 日志。

一个最好的方法:

我在Spring Security 3.2环境中尝试了@RobWinch的方法来解决类似的问题,但该方法似乎不适用于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的代码是:

public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {

    public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";

    public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
        super(clientDetailsService);
    }

    @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);
                return authorizationRequest;
            }
        }

        return super.createAuthorizationRequest(authorizationParameters);
    }
}


TwoFactorAuthenticationFilter的代码是:

//This class is added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
/**
 * Stores the oauth authorizationRequest in the session so that it can
 * later be picked by the {@link com.example.CustomOAuth2RequestFactory}
 * to continue with the authoriztion flow.
 */
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    private OAuth2RequestFactory oAuth2RequestFactory;
    //These next two are added as a test to avoid the compilation errors that happened when they were not defined.
    public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
    public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";

    @Autowired
    public void setClientDetailsService(ClientDetailsService clientDetailsService) {
        oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
        System.out.println(">>>>>>>>>>> List of authorities includes: ");
        for (GrantedAuthority authority : authorities) {
            System.out.println("auth: "+authority.getAuthority() );
        }
        return authorities.stream().anyMatch(
            authority -> 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() ------------------------");
        // Check if the user hasn't done the two factor authentication.
        if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
        System.out.println("++++++++++++++++++++++++ AUTHENTICATED BUT NOT TWO FACTOR +++++++++++++++++++++++++");
        AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
            /* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
               require two factor authenticatoin. */
        System.out.println("======================== twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) is: " + twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) );
        System.out.println("======================== twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) is: " + twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) );
        if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
                twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
                // Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
                // to return this saved request to the AuthenticationEndpoint after the user successfully
                // did the two factor authentication.
                request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);

                // redirect the the page where the user needs to enter the two factor authentiation code
                redirectStrategy.sendRedirect(request, response,
                    ServletUriComponentsBuilder.fromCurrentContextPath()
                        .path(TwoFactorAuthenticationController.PATH)
                        .toUriString());
                return;
            }
        }
        //THE NEXT "IF" BLOCK DOES NOT RESOLVE THE ERROR WHEN UNCOMMENTED
        //if(AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)){
        //    CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
            // this is the value of the token to be included as either a header or an HTTP parameter
        //    response.setHeader("XSRF-TOKEN", token.getToken());
        //}

        filterChain.doFilter(request, response);
    }

    private Map<String, String> paramsFromRequest(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            params.put(entry.getKey(), entry.getValue()[0]);
        }
        return params;
    }
}

在计算机上重新创建问题:

您可以在几分钟内在任何计算机上重现问题,只需遵循以下简单步骤:
1.)下载该应用程序的压缩版本(编者注:不幸的是,这不再可用)
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.)打开Web浏览器并导航到http : // localhost : 8080
7.)单击Login,然后输入Frodo作为用户,MyRing作为密码,然后单击提交。
8.)输入Pin Code作为Pin Code,然后单击提交。这将触发上面显示的错误。
您可以通过以下方式查看完整的源代码:
a.)将maven项目导入IDE,或者
B.)在解压缩的目录中导航并使用文本编辑器打开。

pw9qyyiw

pw9qyyiw1#

我突然想到一个主意:
如果会话固定被激活,在用户成功通过身份验证后,将创建一个新会话(参见SessionFixationProtectionStrategy)。如果您使用默认的HttpSessionCsrfTokenRepository,这当然也会创建一个新的csrf令牌。由于您提到了XSRF-TOKEN头,我假设您使用了一些JavaScript前端。我可以想象用于登录的原始csrf令牌被存储并在之后重用-这将不起作用,因为该CSRF令牌不再有效。
您可以尝试禁用会话固定(http.sessionManagement().sessionFixation().none()<session-management session-fixation-protection="none"/>)或在登录后重新获取当前CSRF令牌。

zrfyljdw

zrfyljdw2#

您的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);
}

字符串

我重新审视这一点,因为我最初的回答草案被否决。这个版本是进一步沿着相同的道路,我相信这是正确的途径。

相关问题