【问题标题】:null client in OAuth2 Multi-Factor AuthenticationOAuth2 多重身份验证中的空客户端
【发布时间】:2016-08-22 07:34:36
【问题描述】:

Spring OAuth2 实现多因素身份验证的完整代码已上传至a file sharing site that you can download by clicking on this link。下面的说明解释了如何使用该链接在任何计算机上重新创建当前问题。 提供 500 点赏金。


当前错误:

当用户尝试在the Spring Boot OAuth2 app from the link in the preceding paragraph 中使用两因素身份验证进行身份验证时,将触发错误。当应用程序应提供第二个页面要求用户输入 PIN 码以确认用户身份时,会在此过程中引发错误。

鉴于空客户端正在触发此错误,问题似乎是如何在 Spring Boot OAuth2 中将 ClientDetailsService 连接到 Custom OAuth2RequestFactory

entire debug log can be read at a file sharing site by clicking on this link。日志中的完整堆栈跟踪仅包含对实际在应用程序中的代码的一个引用,该代码行是:

AuthorizationRequest authorizationRequest =  
oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));

调试日志中抛出的错误是:

org.springframework.security.oauth2.provider.NoSuchClientException:  
No client with requested id: null  


抛出错误时的控制流程:

我创建了以下流程图来说明@James' suggested implementation 中多因素身份验证请求的预期流程:

在前面的流程图中,当前错误是在 用户名和密码视图GET /secure/two_factor_authenticated 步骤之间的某个时间点引发的。

此 OP 的解决方案仅限于 FIRST PASS,即 1.) 通过/oauth/authorize 端点,然后 2.) 通过TwoFactorAuthenticationController 返回/oauth/authorize 端点。强>

所以我们只想解决NoSuchClientException,同时还证明客户端已成功在POST /secure/two_factor_authenticated 中授予ROLE_TWO_FACTOR_AUTHENTICATED。鉴于后续步骤是样板,只要用户输入 SECOND PASS 进入CustomOAuth2RequestFactory,流程明显中断是可以接受的strong> 以及成功完成 FIRST PASS 的所有工件。只要我们在此处成功解决了 FIRST PASSSECOND PASS 可以是一个单独的问题。


相关代码摘录:

这是AuthorizationServerConfigurerAdapter 的代码,我尝试在其中建立连接:

@Configuration
@EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired//ADDED AS A TEST TO TRY TO HOOK UP THE CUSTOM REQUEST FACTORY
    private ClientDetailsService clientDetailsService;

    @Autowired//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
    private CustomOAuth2RequestFactory customOAuth2RequestFactory;

    //THIS NEXT BEAN IS A TEST
    @Bean CustomOAuth2RequestFactory customOAuth2RequestFactory(){
        return new CustomOAuth2RequestFactory(clientDetailsService);
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory(
                    new ClassPathResource("keystore.jks"), "foobar".toCharArray()
                )
                .getKeyPair("test");
        converter.setKeyPair(keyPair);
        return converter;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("acme")//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/builders/ClientDetailsServiceBuilder.ClientBuilder.html
                    .secret("acmesecret")
                    .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                    .scopes("openid");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerEndpointsConfigurer.html
            .authenticationManager(authenticationManager)
            .accessTokenConverter(jwtAccessTokenConverter())
            .requestFactory(customOAuth2RequestFactory);//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.html
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()");
    }

}

这是TwoFactorAuthenticationFilter 的代码,其中包含触发错误的上述代码:

package demo;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

//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) {
        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 {
        // Check if the user hasn't done the two factor authentication.
        if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
            AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
            /* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
               require two factor authenticatoin. */
            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;
            }
        }

        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.) 下载zipped version of the app from a file sharing site by clicking on this link

2.) 输入解压应用程序:tar -zxvf oauth2.tar(1).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.) 点击Login,然后输入Frodo作为用户,MyRing作为密码,然后点击提交。 这将触发上面显示的错误。

您可以通过以下方式查看完整的源代码:

a.) 将 maven 项目导入您的 IDE,或通过

b.) 在解压后的目录中导航并使用文本编辑器打开。

注意:上面文件共享链接中的代码是the Spring Boot OAuth2 GitHub sample at this linksuggestions for 2 Factor Authentication offered by @James at this link的组合。 Spring Boot GitHub 示例的唯一更改是在 authserver 应用程序中,特别是在 authserver/src/main/javaauthserver/src/main/resources/templates 中。


缩小问题范围:


根据@AbrahamGrief 的建议,我添加了FilterConfigurationBean,解决了NoSuchClientException。但是 OP 询问如何通过图表中的控制流程完成 FIRST PASS 以获得 500 点赏金

然后我通过在Users.loadUserByUername() 中设置ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED 来缩小问题范围,如下所示:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    String password;
    List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
    if (username.equals("Samwise")) {//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
        auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
        password = "TheShire";
    }
    else if (username.equals("Frodo")){//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
        auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
        password = "MyRing";
    }
    else{throw new UsernameNotFoundException("Username was not found. ");}
    return new org.springframework.security.core.userdetails.User(username, password, auth);
}

这消除了配置客户端和资源的需要,因此当前的问题仍然很窄。然而,下一个障碍是 Spring Security 拒绝用户对/security/two_factor_authentication 的请求。 还需要进行哪些更改才能通过控制流完成 FIRST PASS,以便 POST /secure/two_factor_authentication 可以 SYSO ROLE_TWO_FACTOR_AUTHENTICATED

【问题讨论】:

    标签: spring spring-mvc spring-security spring-boot spring-security-oauth2


    【解决方案1】:

    该项目需要大量修改来实现所描述的流程,这超出了单个问题的范围。这个答案将只关注如何解决:

    org.springframework.security.oauth2.provider.NoSuchClientException: 否 请求 id 的客户端:null

    在 Spring Boot 授权服务器中运行时尝试使用 SecurityWebApplicationInitializerFilter bean。

    发生此异常的原因是因为WebApplicationInitializer instances are not run by Spring Boot。这包括任何可以在部署到独立 Servlet 容器的 WAR 中工作的 AbstractSecurityWebApplicationInitializer 子类。所以发生的事情是 Spring Boot 由于 @Bean 注释创建了您的过滤器,忽略了您的 AbstractSecurityWebApplicationInitializer,并将您的过滤器应用于所有 URL。同时,您只想将过滤器应用于您尝试传递给 addMappingForUrlPatterns 的那些 URL。

    相反,要将 servlet 过滤器应用于 Spring Boot 中的特定 URL,您应该 define a FilterConfigurationBean。对于问题中描述的流程,即尝试将自定义 TwoFactorAuthenticationFilter 应用于 /oauth/authorize,如下所示:

    @Bean
    public FilterRegistrationBean twoFactorAuthenticationFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(twoFactorAuthenticationFilter());
        registration.addUrlPatterns("/oauth/authorize");
        registration.setName("twoFactorAuthenticationFilter");
        return registration;
    }
    
    @Bean
    public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
        return new TwoFactorAuthenticationFilter();
    }
    

    【讨论】:

    • 这还不是一个 500 分的答案,但可以推动我们前进。我同意我们应该缩小范围,所以我通过在 Users.loadUserByUserName() 中设置用户的角色来缩小剩余的问题,例如else if (username.equals("Frodo")){ auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED"); password = "MyRing";}。但是,/secure/two_factor_authentication 不对ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED 开放。如何获取它以便用户的密码检查导致他们可以访问“/secure/two_factor_authentication”?到目前为止 +1
    • 我在 OP 的末尾添加了一些东西来缩小问题范围。
    • @CodeMed 再向前几步,在AuthserverApplication 中,将.requestMatchers().antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access") 替换为.and().authorizeRequests().antMatchers("/oauth/authorize").permitAll().and().authorizeRequests().antMatchers("/secure/two_factor_authentication", "/pincode").authenticated(),将.authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED) 添加到您的ClientDetailsS​​erviceConfigurer,并在TwoFactorAuthenticationController 中返回"pinCode" 而不是"templates/pinCode".
    • 在修复了几个下游错别字之后,现在解决了 OP 的范围。在完成 OP 的控制流程图中的大部分 SECOND PASS 后,它目前正在给出 Invalid CSRF token found for http://localhost:9999/uaa/oauth/token 错误。这意味着已满足此赏金的要求。最让我感激的是你在赏金期的早期回复,让我不用等整整7天。谢谢你和+525。
    • @turgos 我没有。
    猜你喜欢
    • 2013-07-20
    • 2015-08-11
    • 2019-01-24
    • 1970-01-01
    • 2012-12-28
    • 2016-07-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多