我正在寻找有关如何使用Spring Security OAuth2实现两要素身份验证(2FA)的想法。要求是仅对于具有敏感信息的特定应用程序,用户需要两因素身份验证。这些Web应用程序具有自己的客户端ID。

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

样本流如下所示:

访问不带2FA和带有2FA的应用程序

  • 用户已注销
  • 用户访问不需要2FA的应用A
  • 重定向到OAuth应用,用户使用用户名和密码
  • 登录
  • 重定向回应用程序A,并且用户已登录
  • 用户访问不需要2FA的应用B
  • 重定向到OAuth应用,重定向回应用B,用户直接登录
  • 用户访问需要2FA的应用S
  • 重定向到OAuth应用,用户需要另外提供2FA token
  • 重定向回应用程序S,并且用户已登录

  • 使用2FA直接访问应用程序
  • 用户已注销
  • 用户访问需要2FA的应用S
  • 重定向到OAuth应用,用户使用用户名和密码登录,用户需要另外提供2FA token
  • 重定向回应用程序S,并且用户已登录

  • 您还有其他想法如何解决这个问题吗?

    最佳答案

    因此,这就是最终实现两因素身份验证的方式:

    在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放入 session 中。然后,用户将被重定向到他必须输入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端点。
    @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(如果有)从 session 中检索以前保存的AuthorizationRequest,然后返回该自定义TwoFactorAuthenticationInterceptor;如果在 session 中找不到任何TwoFactorAuthenticationFilter,则创建新的AuthorizationServerConfigurer
    /**
     * 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 config时,您可以创建一个TwoFactorAuthenticationInterceptor而不是TwoFactorAuthenticationFilter并使用preHandle注册它
    @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();
        }
    }
    

    ojit_code在其ojit_code方法中包含与ojit_code相同的逻辑。

    08-26 20:56