使用Spring Security Oauth2进行两因素身份验证 [英] Two factor authentication with spring security oauth2

查看:791
本文介绍了使用Spring Security Oauth2进行两因素身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm looking for ideas how to implement two factor authentication (2FA) with spring security OAuth2. The requirement is that the user needs two factor authentication only for specific applications with sensitive information. Those webapps have their own client ids.

我想到的一个想法是误用"范围批准页面,以迫使用户输入2FA代码/PIN(或其他任何内容).

One idea that popped in my mind would be to "mis-use" the scope approval page to force the user to enter the 2FA code/PIN (or whatever).

样本流如下所示:

在不使用2FA的情况下访问应用程序

  • 用户已注销
  • 用户访问不需要2FA的应用A
  • 重定向到OAuth应用,用户使用用户名和密码登录
  • 重定向回应用A,并且用户已登录
  • 用户访问同样不需要2FA的应用B
  • 重定向到OAuth应用,重定向回应用B,用户直接登录
  • 用户访问需要 的应用S S
  • 重定向到OAuth应用,用户需要另外提供2FA令牌
  • 重定向回应用程序S,并且用户已登录
  • User is logged out
  • User accesses app A which does not require 2FA
  • Redirect to OAuth app, user logs in with username and password
  • Redirected back to app A and user is logged in
  • User accesses app B which also does not require 2FA
  • Redirect to OAuth app, redirect back to app B and user is directly logged in
  • User accesses app S which does require 2FA
  • Redirect to OAuth app, user needs to additionally provide the 2FA token
  • Redirected back to app S and user is logged in

使用2FA直接访问应用

  • 用户已注销
  • 用户访问需要 的应用S S
  • 重定向到OAuth应用,用户使用用户名和密码登录,用户需要另外提供2FA令牌
  • 重定向回应用程序S,并且用户已登录
  • User is logged out
  • User accesses app S which does require 2FA
  • Redirect to OAuth app, user logs in with username and password, user needs to additionally provide the 2FA token
  • Redirected back to app S and user is logged in

您还有其他方法来解决这个问题吗?

Do you have other ideas how to apporach this?

推荐答案

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

So this is how two factor authentication has been implemented finally:

在弹簧安全性过滤器之后为/oauth/authorize路径注册了一个过滤器:

A filter is registered for the /oauth/authorize path after the spring security filter:

@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代码的页面:

This filter checks if the user hasn't already authenticated with a 2nd factor (by checking if the ROLE_TWO_FACTOR_AUTHENTICATED authority isn't available) and creates an OAuth AuthorizationRequest which is put into the session. The user is then redirected to the page where he has to enter the 2FA code:

/**
 * 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端点.

The TwoFactorAuthenticationController that handles entering the 2FA-code adds the authority ROLE_TWO_FACTOR_AUTHENTICATED if the code was correct and redirects the user back to the /oauth/authorize endpoint.

@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.

A custom OAuth2RequestFactory retrieves the previously saved AuthorizationRequest from the session if available and returns that or creates a new one if none can be found in the session.

/**
 * 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设置为授权服务器,例如:

This custom OAuth2RequestFactory is set to the authorization server like:

<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并使用向其注册

When using java config you can create a TwoFactorAuthenticationInterceptor instead of the TwoFactorAuthenticationFilter and register it with an AuthorizationServerConfigurer with

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

TwoFactorAuthenticationInterceptorpreHandle方法包含与TwoFactorAuthenticationFilter相同的逻辑.

The TwoFactorAuthenticationInterceptor contains the same logic as the TwoFactorAuthenticationFilter in its preHandle method.

这篇关于使用Spring Security Oauth2进行两因素身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆