[SPRING] 스프링 보안 oauth2를 사용한 두 가지 요소 인증
SPRING스프링 보안 oauth2를 사용한 두 가지 요소 인증
스프링 보안 OAuth2를 사용하여 두 가지 요소 인증 (2FA)을 구현하는 방법에 대한 아이디어를 찾고 있습니다. 요구 사항은 중요한 정보가있는 특정 응용 프로그램에 대해서만 두 가지 요소 인증이 필요하다는 것입니다. 이러한 웹 응용 프로그램에는 고유 한 클라이언트 ID가 있습니다.
내 생각에 떠오른 아이디어 중 하나는 사용자가 2FA 코드 / PIN (또는 무엇이든)을 입력하도록하는 범위 승인 페이지를 "잘못 사용하는"것입니다.
샘플 플로우는 다음과 같습니다.
2FA를 사용하거나 사용하지 않는 앱 액세스
2FA로 앱에 직접 액세스
이것에 접근하는 다른 아이디어가 있습니까?
해결법
-
==============================
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.나는 받아 들인 해결책을 작동하게 만들 수 없었다. 나는 잠시 동안이 작업을 해왔고 마침내 여기에 설명 된 아이디어를 사용하여 내 솔루션을 작성했습니다.이 스레드는 "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; } }
from https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2 by cc-by-sa and MIT license
'SPRING' 카테고리의 다른 글
[SPRING] web.xml과 같이 spring-boot 서블릿을 설정하는 방법은? (0) | 2019.01.28 |
---|---|
[SPRING] Spring 보안 OAuth2 자원 서버 항상 잘못된 토큰 반환 (0) | 2019.01.28 |
[SPRING] 스프링의 경로 속성 [닫힘] (0) | 2019.01.28 |
[SPRING] 웹 스프링스 봄과 데이터베이스에서 읽기 (0) | 2019.01.28 |
[SPRING] Spring의 JdbcTemplate과 동일한 연결을 재사용하는 방법? (0) | 2019.01.28 |