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

Cod*_*Med 9 spring spring-mvc spring-security spring-boot spring-security-oauth2

Spring OAuth2多因素身份验证实现的完整代码已上载到文件共享站点,您可以通过单击此链接下载该站点.下面的说明解释了如何使用该链接在任何计算机上重新创建当前问题.提供500点赏金.


当前错误:


当用户尝试使用前一段中链接的Spring Boot OAuth2应用程序双因素身份验证进行身份验证时,将触发错误.当应用程序应该提供第二页时要求用户输入密码来确认用户的身份,该过程中会出现错误.

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

整个调试日志可以在文件共享网站点击此链接上读取.日志中的完整堆栈跟踪仅包含对应用程序中实际代码的一个引用,该代码行为:

AuthorizationRequest authorizationRequest =  
oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
Run Code Online (Sandbox Code Playgroud)

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

org.springframework.security.oauth2.provider.NoSuchClientException:  
No client with requested id: null  
Run Code Online (Sandbox Code Playgroud)


错误发生时控制流量:


我创建了以下流程图,以说明@James建议实现中的多因素身份验证请求的预期流程:

在上面的流程图中,在用户名和密码视图GET/secure/two_factor_authenticated步骤之间的某个时刻抛出当前错误.

该OP的解决方案的范围仅限于FIRST PASS,其中1.)穿过/oauth/authorize端点,然后2.)通过端口返回/oauth/authorize端点TwoFactorAuthenticationController.

因此,我们只是想解决NoSuchClientException,同时也表明客户端已成功授权ROLE_TWO_FACTOR_AUTHENTICATEDPOST /secure/two_factor_authenticated.鉴于随后的步骤是锅炉板,只要用户以成功完成FIRST PASS的所有工件进入SECOND PASS,流程就可以在SECOND PASS条目中明显中断.只要我们在这里成功解决FIRST PASS,SECOND PASS就可以成为一个单独的问题. CustomOAuth2RequestFactory


相关代码EXCERPTS:


这是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()");
    }

}
Run Code Online (Sandbox Code Playgroud)

这是代码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;
    }
} 
Run Code Online (Sandbox Code Playgroud)

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


您可以通过以下简单步骤在几分钟内在任何计算机上重新创建问题:

1.)通过单击此链接从文件共享站点下载应用程序压缩版本.

2.)键入以下内容解压缩应用程序: tar -zxvf oauth2.tar(1).gz

3.)authserver通过导航到oauth2/authserver然后键入来启动应用程序mvn spring-boot:run.

4.)resource通过导航到oauth2/resource然后键入来启动应用程序mvn spring-boot:run

5.)ui通过导航到oauth2/ui然后键入来启动应用程序mvn spring-boot:run

6.)打开Web浏览器并导航到 http : // localhost : 8080

7.)单击Login,然后Frodo以用户MyRing身份和密码输入,然后单击以提交. 这将触发上面显示的错误.

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

a.)将maven项目导入IDE,或者

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

注意:上面文件共享链接中的代码是此链接上的Spring Boot OAuth2 GitHub示例的组合,以及@James在此链接上提供的2因素身份验证建议.对Spring Boot GitHub示例的唯一更改已经在authserver应用程序中,特别是在内部authserver/src/main/java和内部authserver/src/main/resources/templates.


解决问题:


按照@ AbrahamGrief的建议,我加了一个FilterConfigurationBean,解决了NoSuchClientException.但是OP询问如何通过图表中的控制流完成FIRST PASS以获得500点奖励.

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

@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);
}
Run Code Online (Sandbox Code Playgroud)

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

hee*_*nee 5

这里有很多需要该项目,以实现所描述的流程,多在范围上应为一个问题的修改。这个答案将只关注如何解决:

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

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

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

相反,要将 servlet 过滤器应用于 Spring Boot 中的特定 URL,您应该定义一个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();
}
Run Code Online (Sandbox Code Playgroud)