OAuth2 多重身份验证中的空客户端

null client in OAuth2 Multi-Factor Authentication

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


当前错误:


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

鉴于空客户端触发此错误,问题似乎是如何在 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  


抛出错误时的控制流程:


我创建了以下流程图来说明 中多重身份验证请求的预期流程:

在前面的流程图中,当前错误在 用户名和密码视图GET /secure/two_factor_authenticated[= 之间的某个点被抛出150=] 步骤。

此 OP 的解决方案仅限于 1.) 通过 /oauth/authorize 端点然后 2.) returns 返回 /oauth/authorize 端点通过 TwoFactorAuthenticationController.

所以我们只想解决NoSuchClientException,同时也证明客户端已在POST /secure/two_factor_authenticated中成功授予ROLE_TWO_FACTOR_AUTHENTICATED。鉴于后续步骤是样板式的,只要用户输入 [=],流程在 SECOND PASS 进入 CustomOAuth2RequestFactory 时明显中断是可以接受的119=]SECOND PASS 以及成功完成 FIRST PASS 的所有工件。 SECOND PASS可以是一个单独的问题,只要我们在这里成功解决FIRST 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: 
    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: 
    }

    @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: 
/**
 * 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.) 在解压缩的目录中导航并使用文本编辑器打开。

注:上面文件分享link中的代码是the Spring Boot OAuth2 GitHub sample at this link, and the . The only changes to the Spring Boot GitHub sample have been in the authserver app, specifically in authserver/src/main/java and in authserver/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/two_factor_authentication 的请求。 为了通过控制流完成 FIRST PASS 需要做哪些进一步的更改,以便 POST /secure/two_factor_authentication 可以 SYSO ROLE_TWO_FACTOR_AUTHENTICATED?

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

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

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

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

的那些 URL

相反,要将 servlet 过滤器应用于 Spring 引导中的特定 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();
}