[英]Two factor authentication with spring security oauth2
我正在尋找有關如何使用spring security OAuth2實現雙因素身份驗證(2FA)的想法。 要求是用戶僅需要對具有敏感信息的特定應用程序進行雙因素身份驗證。 這些webapps有自己的客戶端ID。
我想到的一個想法是“誤用”范圍審批頁面以強制用戶輸入2FA代碼/ PIN(或其他)。
示例流程如下所示:
不使用和使用2FA訪問應用程序
使用2FA直接訪問應用
你有其他想法如何計算這個嗎?
所以這就是最終實現雙因素身份驗證的方式:
在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;
}
}
該TwoFactorAuthenticationController
,處理進入2FA代碼添加權威ROLE_TWO_FACTOR_AUTHENTICATED
如果代碼是正確的,將用戶重定向回/ OAuth的/授權端點。
@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();
}
}
TwoFactorAuthenticationFilter
在其preHandle
方法中包含與TwoFactorAuthenticationInterceptor
相同的邏輯。
我無法使接受的解決方案有效。 我一直在研究這個問題,最后我使用這里解釋的想法並在這個線程“ 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();
}
}
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;
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.