首页 文章

使用spring security oauth2进行双因素身份验证

提问于
浏览
18

我正在寻找有关如何使用spring security OAuth2实现双因素身份验证(2FA)的想法 . 要求是用户仅需要对具有敏感信息的特定应用程序进行双因素身份验证 . 这些webapps有自己的客户端ID .

我想到的一个想法是“误用”范围审批页面以强制用户输入2FA代码/ PIN(或其他) .

示例流程如下所示:

Accessing apps without and with 2FA

  • 用户已注销

  • 用户访问不需要2FA的应用A.

  • 重定向到OAuth应用,用户使用用户名和密码登录

  • 重定向回应用程序A并且用户已登录

  • 用户访问app B,也不需要2FA

  • 重定向到OAuth应用,重定向回应用B,用户直接登录

  • 用户访问需要2FA的应用程序S.

  • 重定向到OAuth应用,用户需要另外提供2FA令牌

  • 重定向回应用程序S并且用户已登录

Directly accessing app with 2FA

  • 用户已注销

  • 用户访问需要2FA的应用程序S.

  • 重定向到OAuth应用,用户使用用户名和密码登录,用户需要另外提供2FA令牌

  • 重定向回应用程序S并且用户已登录

你有其他想法如何计算这个吗?

2 回答

  • 23

    所以这就是最终实现双因素身份验证的方式:

    在spring安全过滤器之后,为/ 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 如果代码正确则添加权限 ROLE_TWO_FACTOR_AUTHENTICATED 并将用户重定向回/ oauth / authorize endpoints .

    @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 (如果可用),如果在会话中找不到,则返回该值或创建新的 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配置时,您可以创建 TwoFactorAuthenticationInterceptor 而不是 TwoFactorAuthenticationFilter 并使用 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 相同的逻辑 .

  • 0

    我无法使接受的解决方案有效 . 我一直在研究这个问题,最后我通过使用这里解释的想法并在这个帖子上写了我的解决方案“null client in OAuth2 Multi-Factor Authentication

    以下是我的工作解决方案的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();
        }
    }
    

    CustomOAuth2RequestFactory

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

    TwoFactorAuthenticationFilter

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

    TwoFactorAuthenticationController

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

相关问题