Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
805 views
in Technique[技术] by (71.8m points)

spring - null client in OAuth2 Multi-Factor Authentication

Complete code for a Spring OAuth2 implementation of multi-factor authentication has been uploaded to a file sharing site that you can download by clicking on this link. Instructions below explain how to use the link to recreate the current problem on any computer. A 500 point bounty is offered.


THE CURRENT ERROR:

An error is being triggered when a user tries to authenticate using two factor authentication in the Spring Boot OAuth2 app from the link in the preceding paragraph. The error is thrown at the point in the process when the app should serve up a second page asking the user for a pin code to confirm the user's identity.

Given that a null client is triggering this error, the problem seems to be how to connect a ClientDetailsService to a Custom OAuth2RequestFactory in Spring Boot OAuth2.

The entire debug log can be read at a file sharing site by clicking on this link. The complete stack trace in the logs contains only one reference to code that is actually in the app, and that line of code is:

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

The error thrown in the debug logs is:

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


CONTROL FLOW WHEN ERROR IS THROWN:

I created the following flowchart to illustrate the intended flow of multi-factor authentication requests in @James' suggested implementation:

In the preceding flowchart, the current error is being thrown at some point between the Username & Password View and the GET /secure/two_factor_authenticated steps.

The solution to this OP is limited in scope to the FIRST PASS that 1.) travels through the /oauth/authorize endpoint and then 2.) returns back to the /oauth/authorize endpoint via TwoFactorAuthenticationController.

So we simply want to resolve the NoSuchClientException while also demonstrating that the client has been successfully granted ROLE_TWO_FACTOR_AUTHENTICATED in the POST /secure/two_factor_authenticated. Given that the subsequent steps are boiler-plate, it is acceptable for the flow to demonstrably break in the SECOND PASS entry into CustomOAuth2RequestFactory, as long as the user enters the SECOND PASS with all the artifacts of successfully having completed the FIRST PASS. The SECOND PASS can be a separate question as long as we successfully resolve the FIRST PASS here.


RELEVANT CODE EXCERPTS:

Here is the code for the AuthorizationServerConfigurerAdapter, where I attempt to set up the connection:

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

}

Here is the code for the TwoFactorAuthenticationFilter, which contains the code above that is triggering the error:

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;
    }
} 

RE-CREATING THE PROBLEM ON YOUR COMPUTER:


You can recreate the problem on any computer in only a few minutes by following these simple steps:

1.) Download the zipped version of the app from a file sharing site by clicking on this link.

2.) Unzip the app by typing: tar -zxvf oauth2.tar(1).gz

3.) launch the authserver app by navigating to oauth2/authserver and then typing mvn spring-boot:run.

4.) launch the resource app by navigating to oauth2/resource and then typing mvn spring-boot:run

5.) launch


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

There are a lot of modifications needed for that project to implement the described flow, more than should be in scope for a single question. This answer will focus solely on how to resolve:

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

when trying to use a SecurityWebApplicationInitializer and a Filter bean while running in a Spring Boot authorization server.

The reason this exception is happening is because WebApplicationInitializer instances are not run by Spring Boot. That includes any AbstractSecurityWebApplicationInitializer subclasses that would work in a WAR deployed to a standalone Servlet container. So what is happening is Spring Boot creates your filter because of the @Bean annotation, ignores your AbstractSecurityWebApplicationInitializer, and applies your filter to all URLs. Meanwhile, you only want your filter applied to those URLs that you're trying to pass to addMappingForUrlPatterns.

Instead, to apply a servlet Filter to particular URLs in Spring Boot, you should define a FilterConfigurationBean. For the flow described in the question, which is trying to apply a custom TwoFactorAuthenticationFilter to /oauth/authorize, that would look as follows:

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...