복붙노트

[SPRING] 스프링 보안 oauth2를 사용한 두 가지 요소 인증

SPRING

스프링 보안 oauth2를 사용한 두 가지 요소 인증

스프링 보안 OAuth2를 사용하여 두 가지 요소 인증 (2FA)을 구현하는 방법에 대한 아이디어를 찾고 있습니다. 요구 사항은 중요한 정보가있는 특정 응용 프로그램에 대해서만 두 가지 요소 인증이 필요하다는 것입니다. 이러한 웹 응용 프로그램에는 고유 한 클라이언트 ID가 있습니다.

내 생각에 떠오른 아이디어 중 하나는 사용자가 2FA 코드 / PIN (또는 무엇이든)을 입력하도록하는 범위 승인 페이지를 "잘못 사용하는"것입니다.

샘플 플로우는 다음과 같습니다.

2FA를 사용하거나 사용하지 않는 앱 액세스

2FA로 앱에 직접 액세스

이것에 접근하는 다른 아이디어가 있습니까?

해결법

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

    1.이것이 두 가지 요소 인증이 마지막으로 구현 된 방법입니다.

    이것이 두 가지 요소 인증이 마지막으로 구현 된 방법입니다.

    스프링 보안 필터 다음에 / oauth / authorize 경로에 대한 필터가 등록됩니다.

    @Order(200)
    public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
            FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));
            twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");
            super.afterSpringSecurityFilterChain(servletContext);
        }
    }
    

    이 필터는 사용자가 ROLE_TWO_FACTOR_AUTHENTICATED 권한을 사용할 수 없는지 확인하여 두 번째 요소로 아직 인증하지 않았는지 확인하고 세션에 넣은 OAuth AuthorizationRequest를 만듭니다. 그러면 사용자가 2FA 코드를 입력해야하는 페이지로 리디렉션됩니다.

    /**
     * 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;
    
        @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;
                } else {
                    request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                }
            }
    
            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;
        }
    }
    

    2FA 코드 입력을 처리하는 TwoFactorAuthenticationController는 코드가 정확하고 사용자를 / oauth / authorize 엔드 포인트로 다시 리디렉션하는 경우 ROLE_TWO_FACTOR_AUTHENTICATED 권한을 추가합니다.

    @Controller
    @RequestMapping(TwoFactorAuthenticationController.PATH)
    public class TwoFactorAuthenticationController {
        private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
    
        public static final String PATH = "/secure/two_factor_authentication";
    
        @RequestMapping(method = RequestMethod.GET)
        public String auth(HttpServletRequest request, HttpSession session, ....) {
            if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
                LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
                throw ....;
            }
            else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
                LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                throw ....;
            }
    
            return ....; // Show the form to enter the 2FA secret
        }
    
        @RequestMapping(method = RequestMethod.POST)
        public String auth(....) {
            if (userEnteredCorrect2FASecret()) {
                AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
                return "forward:/oauth/authorize"; // Continue with the OAuth flow
            }
    
            return ....; // Show the form to enter the 2FA secret again
        }
    }
    

    사용자 정의 OAuth2RequestFactory는 세션에서 사용 가능한 경우 이전에 저장된 AuthorizationRequest를 검색하고이를 반환하거나 세션에서 아무것도 찾을 수없는 경우 새 세션을 만듭니다.

    /**
     * If the session contains an {@link AuthorizationRequest}, this one is used and returned.
     * The {@link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows
     * to redirect the user away from the /oauth/authorize endpoint during oauth authorization
     * and show him e.g. a the page where he has to enter a code for two factor authentication.
     * Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session
     * and continue with the oauth authorization.
     */
    public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
    
        public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
    
        public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
            super(clientDetailsService);
        }
    
        @Override
        public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
            ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
            HttpSession session = attr.getRequest().getSession(false);
            if (session != null) {
                AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                if (authorizationRequest != null) {
                    session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                    return authorizationRequest;
                }
            }
    
            return super.createAuthorizationRequest(authorizationParameters);
        }
    }
    

    이 맞춤 OAuth2RequestFactory는 다음과 같은 인증 서버로 설정됩니다.

    <bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">
        <constructor-arg index="0" ref="clientDetailsService" />
    </bean>
    
    <!-- Configures the authorization-server and provides the /oauth/authorize endpoint -->
    <oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
        user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"
        authorization-request-manager-ref="customOAuth2RequestFactory">
        <oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
        <oauth:implicit />
        <oauth:refresh-token />
        <oauth:client-credentials />
        <oauth:password />
    </oauth:authorization-server>
    

    Java 구성을 사용할 때 TwoFactorAuthenticationFilter 대신 TwoFactorAuthenticationInterceptor를 만들고 AuthorizationServerConfigurer에 등록 할 수 있습니다.

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig implements AuthorizationServerConfigurer {
        ...
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                .addInterceptor(twoFactorAuthenticationInterceptor())
                ...
                .requestFactory(customOAuth2RequestFactory());
        }
    
        @Bean
        public HandlerInterceptor twoFactorAuthenticationInterceptor() {
            return new TwoFactorAuthenticationInterceptor();
        }
    }
    

    TwoFactorAuthenticationInterceptor는 preHandle 메서드에 TwoFactorAuthenticationFilter와 동일한 로직을 포함합니다.

  2. ==============================

    2.나는 받아 들인 해결책을 작동하게 만들 수 없었다. 나는 잠시 동안이 작업을 해왔고 마침내 여기에 설명 된 아이디어를 사용하여 내 솔루션을 작성했습니다.이 스레드는 "OAuth2 다중 인증에서 null 클라이언트"입니다.

    나는 받아 들인 해결책을 작동하게 만들 수 없었다. 나는 잠시 동안이 작업을 해왔고 마침내 여기에 설명 된 아이디어를 사용하여 내 솔루션을 작성했습니다.이 스레드는 "OAuth2 다중 인증에서 null 클라이언트"입니다.

    다음은 나를위한 작업 솔루션을위한 GitHub 위치입니다. https://github.com/turgos/oauth2-2FA

    문제 나 더 나은 접근법을 볼 수 있도록 의견을 보내 주시면 감사하겠습니다.

    아래에서이 솔루션의 주요 구성 파일을 찾을 수 있습니다.

    AuthorizationServerConfig

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private ClientDetailsService clientDetailsService;
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
            security.tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }
    
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients
                    .inMemory()
                    .withClient("ClientId")
                    .secret("secret")
                    .authorizedGrantTypes("authorization_code")
                    .scopes("user_info")
                    .authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)
                    .autoApprove(true);
        }
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
            endpoints
                .authenticationManager(authenticationManager)
                .requestFactory(customOAuth2RequestFactory());
        }
    
    
        @Bean
        public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){
            return new CustomOAuth2RequestFactory(clientDetailsService);
        }
    
        @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();
        }
    }
    

    맞춤 OAuth2RequestFactory

    public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
    
        private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);
    
        public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
    
    
        public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
            super(clientDetailsService);
        }
    
        @Override
        public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
    
            ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
            HttpSession session = attr.getRequest().getSession(false);
            if (session != null) {
                AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                if (authorizationRequest != null) {
                    session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
    
    
                    LOG.debug("createAuthorizationRequest(): return saved copy.");
    
                    return authorizationRequest;
                }
            }
    
            LOG.debug("createAuthorizationRequest(): create");
            return super.createAuthorizationRequest(authorizationParameters);
        }
    
    
    }
    

    WebSecurityConfig

    @EnableResourceServer
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        CustomDetailsService customDetailsService;
    
    
        @Bean
        public PasswordEncoder encoder() {
            return new BCryptPasswordEncoder();
        }
    
    
        @Bean(name = "authenticationManager")
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
          public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/webjars/**");
            web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
          }
    
          @Override
          protected void configure(HttpSecurity http) throws Exception { // @formatter:off
              http.requestMatchers()
                  .antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")
                  .and()
                  .authorizeRequests()
                  .anyRequest()
                  .authenticated()
                  .and()
                  .formLogin().loginPage("/login")
                  .permitAll();
          } // @formatter:on
    
    
    
        @Override
        @Autowired // <-- This is crucial otherwise Spring Boot creates its own
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    //        auth//.parentAuthenticationManager(authenticationManager)
    //                .inMemoryAuthentication()
    //                .withUser("demo")
    //                .password("demo")
    //                .roles("USER");
    
            auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
        }
    }
    

    2 요소 인증 필터

    public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
    
        private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);
    
        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())
            );
        }
    
    
    
        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;
        }
    
    
        @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 (isAuthenticated() && !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 authentication. */
                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);
    
                    LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);
    
                    // redirect the the page where the user needs to enter the two factor authentication code
                    redirectStrategy.sendRedirect(request, response,
                            TwoFactorAuthenticationController.PATH
                               );
                    return;
                } 
            }
    
            LOG.debug("doFilterInternal(): without redirect.");
    
            filterChain.doFilter(request, response);
        }
    
        public boolean isAuthenticated(){
            return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
        }
    
        private boolean hasAuthority(String checkedAuthority){
    
    
            return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
                    authority -> checkedAuthority.equals(authority.getAuthority())
                    );
        }
    
    }
    

    2 팩터 인증 컨트롤러

    @Controller
    @RequestMapping(TwoFactorAuthenticationController.PATH)
    
    public class TwoFactorAuthenticationController {
        private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
    
        public static final String PATH = "/secure/two_factor_authentication";
    
        @RequestMapping(method = RequestMethod.GET)
        public String auth(HttpServletRequest request, HttpSession session) {
            if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {
                LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
    
                //throw ....;
            }
            else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
                LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
                //throw ....;
            }
    
            LOG.debug("auth() HTML.Get"); 
    
            return "loginSecret"; // Show the form to enter the 2FA secret
        }
    
        @RequestMapping(method = RequestMethod.POST)
        public String auth(@ModelAttribute(value="secret") String secret, BindingResult result, Model model) {
            LOG.debug("auth() HTML.Post");
    
            if (userEnteredCorrect2FASecret(secret)) {
                addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
                return "forward:/oauth/authorize"; // Continue with the OAuth flow
            }
    
            model.addAttribute("isIncorrectSecret", true);
            return "loginSecret"; // Show the form to enter the 2FA secret again
        }
    
        private boolean isAuthenticatedWithAuthority(String checkedAuthority){
    
            return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
                    authority -> checkedAuthority.equals(authority.getAuthority())
                    );
        }
    
        private boolean addAuthority(String authority){
    
            Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);
            List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
            updatedAuthorities.add(newAuthority);
            updatedAuthorities.addAll(oldAuthorities);
    
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(
                            SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
                            SecurityContextHolder.getContext().getAuthentication().getCredentials(),
                            updatedAuthorities)
            );
    
            return true;
        }
    
        private boolean userEnteredCorrect2FASecret(String secret){
            /* later on, we need to pass a temporary secret for each user and control it here */
            /* this is just a temporary way to check things are working */
    
            if(secret.equals("123"))
                return true;
            else;
                return false;
        }
    }
    
  3. from https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2 by cc-by-sa and MIT license