복붙노트

[SPRING] OAuth2 다중 요소 인증에서 null 클라이언트

SPRING

OAuth2 다중 요소 인증에서 null 클라이언트

스프링 OAuth2 다중 요소 인증 구현을위한 완벽한 코드가이 링크를 클릭하여 다운로드 할 수있는 파일 공유 사이트에 업로드되었습니다. 아래 지침은 링크를 사용하여 모든 컴퓨터에서 현재 문제를 재현하는 방법을 설명합니다. 500 포인트 현상금이 제공됩니다.

현재 오류 :

사용자가 이전 단락의 링크에서 Spring Boot OAuth2 앱의 두 가지 요소 인증을 사용하여 인증을 시도하면 오류가 발생합니다. 이 오류는 앱에서 사용자의 신원을 확인하는 핀 코드를 묻는 두 번째 페이지를 제공해야하는 시점에서 발생합니다.

null 클라이언트가이 오류를 발생 시킨다면 문제는 SpringBoot OAuth2에서 ClientDetailsService를 사용자 정의 OAuth2RequestFactory에 연결하는 방법 인 것 같습니다.

이 링크를 클릭하면 파일 공유 사이트에서 전체 디버그 로그를 읽을 수 있습니다. 로그의 전체 스택 추적에는 실제로 앱에있는 코드에 대한 참조가 하나만 포함되어 있으며 그 코드 행은 다음과 같습니다.

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

디버그 로그에서 발생하는 오류는 다음과 같습니다.

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

오류가 발생하면 제어 흐름 :

@James의 제안 된 구현에서 다중 요소 인증 요청의 의도 된 흐름을 설명하기 위해 다음 순서도를 만들었습니다.

앞의 플로우 차트에서 Username & Password View와 GET / secure / two_factor_authenticated 단계 사이의 어느 시점에서 현재 오류가 발생합니다.

이 OP에 대한 해결책은 범위가 1에서 / oauth / authorize 끝 점을 통과 한 다음 2)는 TwoFactorAuthenticationController를 통해 / oauth / authorize 끝점으로 돌아 오는 첫 번째 단계로 제한됩니다.

따라서 NoSuchClientException을 해결하고 POST / secure / two_factor_authenticated에서 클라이언트가 ROLE_TWO_FACTOR_AUTHENTICATED를 성공적으로 부여 받았음을 보여 주기만하면됩니다. 후속 단계가 보일러 플레이트 인 경우 사용자가 첫 번째 패스를 성공적으로 완료 한 모든 아티팩트와 함께 두 번째 패스를 입력하는 경우 흐름이 두 번째 패스 항목에서 CustomOAuth2RequestFactory로 명백하게 파손되는 것이 허용됩니다. SECOND 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.)이 링크를 클릭하여 파일 공유 사이트에서 응용 프로그램의 압축 된 버전을 다운로드하십시오.

2.) 다음을 입력하여 응용 프로그램의 압축을 풉니 다. tar -zxvf oauth2.tar (1) .gz

3.) oauth2 / authserver로 이동 한 다음 mvn spring-boot :를 입력하여 authserver 응용 프로그램을 실행하십시오.

4.) oauth2 / resource로 이동 한 다음 mvn을 입력하여 리소스 앱을 시작합니다. spring-boot : run

5.) oauth2 / ui로 이동 한 다음 mvn spring-boot :를 입력하여 UI 응용 프로그램을 시작합니다

6.) 웹 브라우저를 열고 http : // localhost : 8080으로 이동하십시오

7.) 로그인을 클릭 한 다음 사용자로 Frodo를 입력하고 암호로 MyRing을 입력하고 제출을 클릭하십시오. 위의 오류가 트리거됩니다.

다음과 같은 방법으로 전체 소스 코드를 볼 수 있습니다.

a.) maven 프로젝트를 IDE로 가져 오거나,

b.) 압축 해제 된 디렉토리를 탐색하고 텍스트 편집기로여십시오.

참고 : 위 파일 공유 링크의 코드는이 링크에있는 Spring Boot OAuth2 GitHub 샘플과이 링크에서 @James가 제공 한 2 팩터 인증에 대한 제안입니다. Spring 부트 GitHub 샘플의 유일한 변경 사항은 authserver 응용 프로그램, 특히 authserver / src / main / java 및 authserver / src / main / resources / templates에 있습니다.

문제를 파악하기 : Per @ AbrahamGrief의 제안에 따라 FilterConfigurationBean을 추가하여 NoSuchClientException을 해결했습니다. 그러나 OP는 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에 대한 사용자의 요청을 거부한다는 것입니다. POST / secure / two_factor_authentication이 SYSO ROLE_TWO_FACTOR_AUTHENTICATED 될 수 있도록 제어 흐름을 통해 FIRST PASS를 완성하기 위해 더 많은 변경이 필요합니까?

해결법

  1. ==============================

    1.설명 된 흐름을 구현하기 위해 프로젝트에 필요한 수정이 많이 필요합니다. 단일 질문의 범위를 벗어나지 않아야합니다. 이 답변은 해결 방법에만 초점을 맞 춥니 다.

    설명 된 흐름을 구현하기 위해 프로젝트에 필요한 수정이 많이 필요합니다. 단일 질문의 범위를 벗어나지 않아야합니다. 이 답변은 해결 방법에만 초점을 맞 춥니 다.

    스프링 부트 인증 서버에서 실행되는 동안 SecurityWebApplicationInitializer와 필터 빈을 사용하려고 할 때.

    이 예외가 발생하는 이유는 WebApplicationInitializer 인스턴스가 SpringBoot에 의해 실행되지 않기 때문입니다. 여기에는 독립 서블릿 컨테이너에 배포 된 WAR에서 작동하는 모든 AbstractSecurityWebApplicationInitializer 하위 클래스가 포함됩니다. 따라서 Spring Boot는 @Bean 어노테이션으로 인해 필터를 만들고, AbstractSecurityWebApplicationInitializer를 무시하고 모든 URL에 필터를 적용합니다. 그 사이에 addMappingForUrlPatterns에 전달하려는 URL에만 필터를 적용해야합니다.

    대신 Spring Boot에서 특정 URL에 서블릿 필터를 적용하려면 FilterConfigurationBean을 정의해야한다. / oauth / authorize에 사용자 정의 TwoFactorAuthenticationFilter를 적용하려고하는 질문에 설명 된 흐름의 경우 다음과 같이 표시됩니다.

    @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();
    }
    
  2. from https://stackoverflow.com/questions/36899386/null-client-in-oauth2-multi-factor-authentication by cc-by-sa and MIT license