I am writing a small application for myself and want to implement 2-factor authentication in Spring Boot. To do this, follow the tips of this article: https://www.baeldung.com/spring-security-two-factor-authentication-with-soft-token
Faced the following problems: 1) My code written based on this article does not work. Spring Security completely ignores is2FaEnabled == true and in any case authorizes the user, even if the code has not been entered.
Judging by the logs in this thread .authenticationDetailsSource (authenticationDetailsSource) does not even go for verification.
2) How can I implement the following: during authorization, first check if 2FA is enabled, if so, then direct the user to another URL or open a module with input and after correctly entering the code, authorize it?
Here is my source code:
CustomWebAuthenticationDetailsSource.java
@Component
public class CustomWebAuthenticationDetailsSource implements
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomWebAuthenticationDetails(context);
}
}
CustomWebAuthenticationDetails.java
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
@Getter
private String verificationCode;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
verificationCode = request.getParameter("code");
}
}
CustomAuthenticationProvider.java
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
@Autowired
private UserServiceImpl userServiceImpl;
private Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
User user = userServiceImpl.findUserByEmail(auth.getName());
String verificationCode
= ((CustomWebAuthenticationDetails) auth.getDetails())
.getVerificationCode();
if ((user == null)) {
throw new BadCredentialsException("Invalid username or password");
}
if (user.getIs2FaEnabled()) {
Totp totp = new Totp(user.getTwoFaSecret());
if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) {
throw new BadCredentialsException("Invalid verfication code");
}
}
Authentication result = super.authenticate(auth);
return new UsernamePasswordAuthenticationToken(
user, result.getCredentials(), result.getAuthorities());
}
private boolean isValidLong(String code) {
try {
Long.parseLong(code);
} catch (NumberFormatException e) {
return false;
}
return true;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
// n.b https://stackoverflow.com/questions/1018797/can-you-use-autowired-with-static-fields
private static UserDetailsServiceImpl userDetailsService;
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@PostConstruct
private void init() {
userDetailsService = this.userDetailsServiceImpl;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// Backend configuration
@Configuration
@Order(1)
public static class BackendConfigurationAdapter extends WebSecurityConfigurerAdapter {
/*@Autowired
private UserDetailsServiceImpl userDetailsService;*/
@Autowired
private CustomWebAuthenticationDetailsSource authenticationDetailsSource;
public BackendConfigurationAdapter() {
super();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/admin/**")
.antMatcher("/admin/**/**")
.authorizeRequests()
.anyRequest()
.hasAuthority("ADMIN_PRIVILEGE")
.and()
.formLogin()
.authenticationDetailsSource(authenticationDetailsSource)
.loginPage("/admin/login")
.loginProcessingUrl("/admin/login")
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/admin/dashboard")
.failureUrl("/admin/login?authError")
.permitAll()
.and()
.rememberMe()
.rememberMeParameter("remember-me")
.tokenValiditySeconds(86400)
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/admin/logout"))
.logoutSuccessUrl("/admin/login")
.deleteCookies("JSESSIONID")
.and()
.exceptionHandling()
.accessDeniedPage("/403")
.and()
.csrf()
.ignoringAntMatchers("/admin/**");
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/backend/css/**",
"/backend/js/**",
"/backend/fonts/**",
"/backend/images/**",
"/backend/init/**"
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
// Frontend Configuration
@Configuration
@Order(2)
public static class FrontendConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private CustomWebAuthenticationDetailsSource authenticationDetailsSource;
public FrontendConfigurationAdapter() {
super();
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().mvcMatchers("/robots.txt").permitAll()
.antMatchers(
"/", "/auth", "/signup", "/restore", "/activation/**",
"/admin/login", "/admin_restore",
"/attachments/get/**",
"/sendMessage",
"/error",
"/page/**",
"/categories", "/categories/**",
"/terms/**", "/posts", "/posts/**"
).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.authenticationDetailsSource(authenticationDetailsSource)
.loginPage("/auth")
.loginProcessingUrl("/auth")
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/auth?authError")
.permitAll()
.and()
.rememberMe()
.rememberMeParameter("remember-me")
.tokenValiditySeconds(86400)
.and()
.oauth2Login().defaultSuccessUrl("/")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.and()
.exceptionHandling()
.accessDeniedPage("/403")
.and()
.csrf()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.headers().frameOptions().sameOrigin();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/frontend/css/**",
"/frontend/js/**",
"/frontend/fonts/**",
"/frontend/images/**",
"/frontend/lib/**",
"/frontend/vendor/**"
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
}
Thanks in advance!