Spring Security custom AuthenticationProvider authenticate method called twice - spring

I am developing a Spring Boot that uses an API Key to authenticate. I have created a custom Authentication provider and the authenticate method is called twice. Can anyone tell me why it's being called twice?
This is my authenticate method:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ApiAuthenticationToken authenticationToken = (ApiAuthenticationToken) authentication;
/**
* Authenticate the token
*/
ValidateApiKeyRequest request = new ValidateApiKeyRequest(authenticationToken.getApiKey());
ValidateApiKeyResp resp = getValidateApiKeyCommand().execute(request);
/**
* Populate and return a new authenticaiton token
*/
return createSuccessAuthentication(resp);
}
and this is the createSuccessAuthentication method:
protected Authentication createSuccessAuthentication(final ValidateApiKeyResp resp) {
List<GrantedAuthority> authorities = Lists.newArrayList();
authorities.add(new SimpleGrantedAuthority("API_KEY"));
return new ApiAuthenticationToken(resp.getApiKey(), authorities, true);
}
this is the ApiAuthenticationToken constructor:
public ApiAuthenticationToken(final ApiKey apiKey, Collection<? extends GrantedAuthority> authorities, boolean authenticated) {
super(authorities);
setAuthenticated(true);
this.apiKey = apiKey;
}
This is my security configuration:
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher(CONFIGURATION_MATCHER)
.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint())
.and()
.addFilterBefore(apiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests().antMatchers(CONFIGURATION_MATCHER).authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(apiKeyAuthenticationProvider());

Just in case anyone else has this issue:
The problem was related to my spring security configuration. I had several methods annotated with #Bean - see below
#Bean
public ApiKeyAuthenticationProvider apiKeyAuthenticationProvider() {
return new ApiKeyAuthenticationProvider(getValidateApiKeyCommand());
}
#Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
#Bean
public ApiKeyAuthenticationFilter apiKeyAuthenticationFilter() throws Exception {
ApiKeyAuthenticationFilter apiKeyAuthenticationFilter = new ApiKeyAuthenticationFilter();
apiKeyAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
apiKeyAuthenticationFilter.setAuthenticationSuccessHandler(new ApiKeyAuthenticationSuccessHandler());
apiKeyAuthenticationFilter.setAuthenticationFailureHandler(new ApiKeyAuthenticationFailureHandler());
return apiKeyAuthenticationFilter;
}
But theses beans were getting registered again in the configure(HttpSecurity http) method.
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher(CONFIGURATION_MATCHER)
.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint())
.and()
.addFilterBefore(apiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests().antMatchers(CONFIGURATION_MATCHER).authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(apiKeyAuthenticationProvider());
}
The fix was to remove the #Bean annotation. Seems obvious now :)

Related

Request method 'POST' is not supported

I'm trying to upgrade Spring Boot from 2.7.6 to 3.0.1. I have a problem during the login action. The following is my new WebSecurityConfig:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;
public WebSecurityConfig(CustomUserDetailsService customUserDetailsService, CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler) {
this.customUserDetailsService = customUserDetailsService;
this.customizeAuthenticationSuccessHandler = customizeAuthenticationSuccessHandler;
}
#Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AccessDeniedHandler accessDeniedHandler(){
return new CustomAccessDeniedHandler();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests()
.requestMatchers("/").permitAll()
.requestMatchers("/login").permitAll()
.authenticated()
.and()
.csrf().disable()
.formLogin()
.successHandler(customizeAuthenticationSuccessHandler)
.loginPage("/login")
.failureUrl("/login?error=true")
.usernameParameter("email")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.invalidateHttpSession(true)
.logoutSuccessUrl("/login?logout=true")
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.authenticationProvider(authenticationProvider());
http
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/login?expired=true");
return http.build();
}
// This second filter chain will secure the static resources without reading the SecurityContext from the session.
#Bean
#Order(0)
SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
}
}
Follow my CustomUserDetailService:
#Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserByEmail(String email) {
System.out.println(email);
User user = userRepository.findByEmail(email.toLowerCase());
System.out.println(user.getEmail());
return userRepository.findByEmail(email.toLowerCase());
}
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email.toLowerCase());
if (user != null) {
List<GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority( user.getRole()));;
return buildUserForAuthentication(user, authorities);
} else {
throw new UsernameNotFoundException("username not found");
}
}
private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) {
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
}
}
When I run the application I see the login page, but when I enter the credential and press submit I receive the error:
Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported]
and Tomcat shows:
HTTP Status 405 – Method Not Allowed Type Status Report
Message Method 'POST' is not supported.
I searched for a solution but really I don't understand where is the problem.
To use multiple HttpSecurity instances, you must specify a security matcher, otherwise the first SecurityFilterChain will process all requests, and no requests will reach the second chain.
See this section of the Spring Security reference documentation.
In your case the SecurityFilterChain called resources is matching all requests, because you don't have a security matcher.
Since the resources chain does not configure formLogin then Spring Security does not create the default /login POST endpoint.
You can fix this by changing requests to:
#Bean
#Order(0)
SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.securityMatchers((matchers) -> matchers
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**") // the requests that this SecurityFilterChain will process
)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
}
If you want more details on the difference between authorizeHttpRequests and requestMatchers you can check out this question.
This error typically occurs when the method in the controller is not mapped to a post request. Should be something like:
#RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.POST})
public ModelAndView login(...

Spring Boot with Spring Security - Two Factor Authentication with SMS/ PIN/ TOTP

I'm working on a Spring Boot 2.5.0 web application with Spring Security form login using Thymeleaf. I'm looking for ideas on how to implement two factor authentication (2FA) with spring security form login.
The requirement is that when a user logs in with his username and password via. the login form, if the username and password authentication is successful an SMS code should be sent to the registered mobile number of the user and he should be challenged with another page to enter the SMS code. If user gets the SMS code correctly, he should be forwarded to the secured application page.
On the login form, along with the username and password, the user is also requested to enter the text from a captcha image which is verified using a SimpleAuthenticationFilter which extends UsernamePasswordAuthenticationFilter.
This is the current SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsServiceImpl userDetailsService;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers(
"/favicon.ico",
"/webjars/**",
"/images/**",
"/css/**",
"/js/**",
"/login/**",
"/captcha/**",
"/public/**",
"/user/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/", true)
.and().logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSONID")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.permitAll()
.and().headers().frameOptions().sameOrigin()
.and().sessionManagement()
.maximumSessions(5)
.sessionRegistry(sessionRegistry())
.expiredUrl("/login?error=5");
}
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(authenticationFailureHandler());
return filter;
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userDetailsService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
/** TO-GET-SESSIONS-STORED-ON-SERVER */
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
}
And this is the SimpleAuthenticationFilter mentioned above.
public class SimpleAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "captcha";
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
HttpSession session = request.getSession(true);
String captchaFromSession = null;
if (session.getAttribute("captcha") != null) {
captchaFromSession = session.getAttribute("captcha").toString();
} else {
throw new CredentialsExpiredException("INVALID SESSION");
}
String captchaFromRequest = obtainCaptcha(request);
if (captchaFromRequest == null) {
throw new AuthenticationCredentialsNotFoundException("INVALID CAPTCHA");
}
if (!captchaFromRequest.equals(captchaFromSession)) {
throw new AuthenticationCredentialsNotFoundException("INVALID CAPTCHA");
}
UsernamePasswordAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
return new UsernamePasswordAuthenticationToken(username, password);
}
private String obtainCaptcha(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_CAPTCHA_KEY);
}
}
Any ideas on how to approach this ? Thanks in advance.
Spring Security has an mfa sample to get you started. It uses Google Authenticator with an OTP, but you can plug in sending/verifying your SMS short-code instead.
You might also consider keeping the captcha verification separate from the (out of the box) authentication filter. If they are separate filters in the same filter chain, it will have the same effect with less code.

How to apply a security filter only on a restricted http path [duplicate]

This question already has answers here:
Disabling a filter for only a few paths in spring security
(2 answers)
Closed 1 year ago.
I have a problem with my Spring security configuration. I just want to basically apply an authentication filter to some paths, and not to other path. But the filter i have defined is applied on all the HTTP request ever what i write in the configuration.
Here is my code.
SecurityConfig:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtFilter jwtFilter;
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("{bcrypt}$2a$10$DmzAlIznZz3faNQx1eBTBOw6fNiGE105fKoHkvskYTMXH5OFUE6iy")
.roles("USER");
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.antMatcher("/admin/**")
.authorizeRequests() //
.anyRequest().authenticated() //
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
#Override
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
JWTFilter:
#Component
public class JwtFilter extends OncePerRequestFilter {
#Autowired
private JWTUtils jwtUtils;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
String authorizationHeader = httpServletRequest.getHeader("Authorization");
String token = null;
String userName = null;
if (authorizationHeader != null) {
userName = jwtUtils.extractUsername(token);
}
if (jwtUtils.validateToken(token)) {
} else {
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userName, null);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
For example if i try to call this servlet:
#PostMapping("/login")
public ResponseEntity<UserDetails> login(#RequestBody User user) throws Exception {
try {
Authentication authenticate = authenticate(user.getName(), user.getPassword());
UserDetails authenticatedUser = (UserDetails) authenticate.getPrincipal();
return ResponseEntity.ok()
.header(
HttpHeaders.AUTHORIZATION,
generateToken(authenticatedUser.getUsername())
)
.body(authenticatedUser);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
My filter is called to verify if the client is authenticated but it is my login end point so my client is accordingly not authenticated yet...
For me the code I found on internet that should resolve this problem is this one:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().disable()
.antMatcher("/admin/**")
.authorizeRequests() //
.anyRequest().authenticated() //
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
You can find this code in the security config.
To exclude urls for the security filter, you should override the other configure method that accepts a WebSecurity as an argument and specify the url paths to ignore...
eg.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/login/**");
}
Another option you could look into is to configure form based login in spring security...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.and()
...
.logout();
}
It also looks like you're custom coding oauth2 authentication. Have you looked at what spring security 5 provides out of the box for securing urls with jwt tokens?
Check out the documentation at
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver.

Spring Security basic auth for REST Api single login needed?

I am using Spring Security basic auth for my REST API.
Initially I get unauthorized HTTP response status for unauthenticated secured routes.
If I provide the right credentials, I get a Ok HTTP response status, but after a single successful login, I can access all the secured routes without providing user credentials.
Here are my questions:
Is it the correct behaviour for basic authentication?
Why it happens?
My security config:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
//J-
http.csrf().disable()
.authorizeRequests()
.antMatchers("/save")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/h2-console/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
//J+
//adding support for h2 console, otherwise crashes
http.headers().frameOptions().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider());
}
}
and here is the UserDetailsService's loadByUsername() method:
#Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UserNotFoundException(username);
} else if (UserStatus.Deactivated.equals(user.getStatus())) {
throw new UserDeactivatedException(username);
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.singleton(new SimpleGrantedAuthority("USER")));
}
https://www.baeldung.com/spring-security-session
Refer mentioned link. For Restful API’s use stateless session policy

Auth websocket session after manual web auth

I am using Spring Security with STOMP WebSocket on SpringBoot. Auth on websocket worked fine with this config when I used simple login form:
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/webjars/**", "/resources/**").permitAll()
.antMatchers("/register").anonymous()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler(customLoginSuccessHandler)
.failureUrl("/login?error")
.permitAll()
.and()
.csrf().disable()
.logout().logoutSuccessHandler(customLogoutSuccessHandler);
}
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated()
.simpTypeMatchers(CONNECT).authenticated()
.simpSubscribeDestMatchers(Channel.SYSTEM_ERROR.value()).permitAll()
.simpDestMatchers("/app/publish*").hasRole("USER")
.simpSubscribeDestMatchers("/user/**", "/topic/**", "/system/*").hasRole("USER")
.anyMessage().denyAll();
}
But when I wanted to manually auth client after register new user in RegisterController:
#RequestMapping(value = "/register", method = RequestMethod.POST)
public String signup(#Valid #ModelAttribute SignupForm signupForm, Errors errors) {
if (errors.hasErrors()) {
return SIGNUP_VIEW_NAME;
}
User user = signupForm.createAccount();
try {
userService.persist(user);
} catch (EntityExistsException ex) {
errors.rejectValue("login", "user.exists");
return SIGNUP_VIEW_NAME;
}
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, Collections.singletonList(new SimpleGrantedAuthority("USER"))));
return "redirect:/";
}
I've got problem with auth websocket. When I get redirected to page where websocket connects I am getting org.springframework.security.access.AccessDeniedException: Access is denied
So. Problem was in define Role. In controller when I defined new SimpleGrantedAuthority("USER") it should be "ROLE_USER" because Spring adds refix ROLLE_ by default. Sure we can change default behaviour of this by add next in WebSecurity configuration
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/favicon.ico");
web.expressionHandler(new DefaultWebSecurityExpressionHandler() {
#Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(authentication, fi);
root.setDefaultRolePrefix(""); //remove the prefix ROLE_
return root;
}
});
}
. Yes, dummy mistake but so common. So I will leave it here

Resources