I'm trying to secure my website using Spring Security following the guides on the web.
So on my server side I have the following classes.
My WebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ApplicationContextAware {
#Override
protected void registerAuthentication(AuthenticationManagerBuilde rauthManagerBuilder) throws Exception {
authManagerBuilder.inMemoryAuthentication().withUser("user").password("password").roles("ADMIN");
}
}
My controller:
#Controller
//#RequestMapping("/course")
public class CourseController implements ApplicationContextAware {
#RequestMapping(value="/course", method = RequestMethod.GET, produces="application/json")
public #ResponseBody List<Course> get( // The criterion used to find.
#RequestParam(value = "what", required = true) String what,
#RequestParam(value = "value", required = true) String value) {
//.....
}
#RequestMapping(value = "/course", method = RequestMethod.POST, produces = "application/json")
public List<Course> upload(#RequestBody Course[] cs) {
}
}
What confused me very much is the server does not respond to the POST/DELETE method, while the GET method works fine. BTW, I'm using RestTemplate on the client side.
Exceptions are:
Exception in thread "main" org.springframework.web.client.HttpClientErrorException: 403 Forbidden
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:574)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:530)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:487)
at org.springframework.web.client.RestTemplate.delete(RestTemplate.java:385)
at hello.Application.createRestTemplate(Application.java:149)
at hello.Application.main(Application.java:99)
I've searched the internet for days. Still don't have a clue. Please help. Thanks so much
The issue is likely due to CSRF protection. If users will not be using your application in a web browser, then it is safe to disable CSRF protection. Otherwise you should ensure to include the CSRF token in the request.
To disable CSRF protection you can use the following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter implements ApplicationContextAware {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.csrf().disable();
}
#Override
protected void registerAuthentication(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder
.inMemoryAuthentication()
.withUser("user").password("password").roles("ADMIN");
}
}
The issue is likely due to CSRF protection, agree with the top comment. Nevertheless, by using this configuration, the method cancells the spring security.
So you can use the following code:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
auth
.inMemoryAuthentication()
.withUser("admin")
.password(encoder.encode("admin"))
.roles("ADMIN", "USER")
.and()
.withUser("user")
.password(encoder.encode("password"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
http.csrf().disable();
}
}
The issue may be related to CSRF or CORS Security Protection.
FOR CSRF: You can disable it if the application users did not use it from browsers.
For CORS: You can specify the origin and allow HTTP Methods.
The below code disable CSRF and allow all origins and HTTP methods. so be aware when using it.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*");
}
}
I've been looking for days too! Simply disabling CSRF on your configure method with http.csrf().disable(); is all that needed to be done for my put requests to stop receiving 403.
Check your token which you are sending through 'Header' and also query in your database for the same token whether that token exist or not.
Note : The above is applicable only in case you are using Spring Boot token authentication mechanism.
I'm posting this in case someone else finds it useful in the future. I spent hours looking for what was failing and finally I find the solution, on Postman making a POST to http://localhost:8080/login/ is not the same as making a POST to http://localhost:8080/login (with "/" at the end of request it will return 403 forbidden)
I had this problem after upgrading my service to Spring Boot 3. Automatic tests started to fail with a 403 status. After quite a lot of headaches, I found out it is caused by removing trailing slash from URL matching.
The change is described here. So check that you are calling the correct URL.
Wrong:
/api/foo/
Right:
/api/foo
http.httpBasic().disable();
http.authorizeRequests().antMatchers("/signup").permitAll().antMatchers("/*")
.fullyAuthenticated().and().formLogin()
.and().csrf().disable();
http.csrf().disable();
Related
I'm developing a REST API based on Spring Boot (spring-boot-starter-web) where I use Spring Security (spring-security-core e spring-security-config) to protect the different endpoints.
The authentication is done by using a local database that contains users with two different sets of roles: ADMIN andUSER. USER should be able toGET all API endpoints and POST to endpoints based onrouteA. ADMIN should be able to do the same asUSER plus POST andDELETE to endpoints based on `routeB
However the behavior I'm getting is that I can do GET requests to any endpoint but POST requests always return HTTP 403 Forbidden for either type of user - ADMIN and USER - which is not expected what I'm expecting based on my SecurityConfiguration.
Any ideas of what am I missing?
SecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private DataSource dataSource;
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
logger.info("Using database as the authentication provider.");
builder.jdbcAuthentication().dataSource(dataSource).passwordEncoder(new BCryptPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
authorizeRequests().antMatchers(HttpMethod.GET, "/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeA/*").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeB/*").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/routeB/*").hasRole("ADMIN").and().
requestCache().requestCache(new NullRequestCache()).and().
httpBasic().authenticationEntryPoint(authenticationEntryPoint).and().
cors();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
RouteBController .java
#RestController
public class RouteBController {
static final Logger logger = LoggerFactory.getLogger(RouteBController.class);
public RouteBController() { }
#RequestMapping(value = "routeB", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET)
public String getStuff() {
return "Got a hello world!";
}
#RequestMapping(value = "routeB", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
public String postStuff() {
return "Posted a hello world!";
}
}
RESTAuthenticationEntryPoint.java
#Component
public class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("AppNameHere");
super.afterPropertiesSet();
}
}
BEFORE disabling the CSFR as a way of fixing this issue, please check the resources on Mohd Waseem's answer to better understand why it is important and to have an idea of how it can be properly set up. As RCaetano has said, CSFR is here to help us from attacks and it should not be disabled blindly.
Since this answer still explained the 2 issues on my original questions, I'll leave it as the marked answer to create awareness about possible issues with the CSFT and security routes but don't take it literally.
There were 2 issues in SecurityConfiguration.java that made it misbehave.
Although the 403 Forbidden error message didn't contain any message indication of why it was failing (see example below) it turns out it was due to having CSRF enabled. Disabling it allowed for POST and DELETE requests to be processed.
{
"timestamp": "2018-06-26T09:17:19.672+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/routeB"
}
Also the expression used in antMatched(HttpMethod, String) for RouteB was incorrect because /routeB/* expects it to have something after /. The correct configurtion is /routeB/** since more paths can be present (or not).
The corrected SecurityConfiguration.java is
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
authorizeRequests().antMatchers(HttpMethod.GET, "/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeA/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeB/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/routeB/**").hasRole("ADMIN").and().
requestCache().requestCache(new NullRequestCache()).and().
httpBasic().authenticationEntryPoint(authenticationEntryPoint).and().
cors().and().
csrf().disable();
}
Source: StackOverflow em Português
Cross-site request forgery is a web security vulnerability that allows an attacker to induce users to perform actions that they do not
intend to perform.
In your case disabling CSRF protection exposes user to this vulnerability.
Note: If it was pure Rest API with O-Auth protection then CSRF was not
needed. Should I use CSRF protection on Rest API endpoints?
But In your case when user logs in a session is created and cookie is returned in response and without CSRF token Attacker can exploit it and perform CSRF.
It wouldn't be a good idea to disable CSRF instead you can configure your app to return CSRF token in response headers and then use it in all your subsequent state changing calls.
Add this line of code in your SecurityConfiguration.java
// CSRF tokens handling
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
CsrfTokenResponseHeaderBindingFilter.java
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";
protected static final String RESPONSE_HEADER_NAME = "X-CSRF-HEADER";
protected static final String RESPONSE_PARAM_NAME = "X-CSRF-PARAM";
protected static final String RESPONSE_TOKEN_NAME = "X-CSRF-TOKEN";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, javax.servlet.FilterChain filterChain) throws ServletException, IOException {
CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
if (token != null) {
response.setHeader(RESPONSE_HEADER_NAME, token.getHeaderName());
response.setHeader(RESPONSE_PARAM_NAME, token.getParameterName());
response.setHeader(RESPONSE_TOKEN_NAME, token.getToken());
}
filterChain.doFilter(request, response);
}
}
Header Response form Server:
Note that we now have CSRF token in the header. This will not change untill the session expires.
Also read: Spring Security’s CSRF protection for REST services: the client side and the server side for better understanding.
It's simple CSRF enabled issue that doesn't allow POST requests. I faced the same problem here's the solution: (Explained)
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/form").hasRole("ADMIN") // Specific api method request based on role.
.antMatchers("/home","/basic").permitAll() // permited urls to guest users(without login).
.anyRequest().authenticated()
.and()
.formLogin() // not specified form page to use default login page of spring security
.permitAll()
.and()
.logout().deleteCookies("JSESSIONID") // delete memory of browser after logout
.and()
.rememberMe().key("uniqueAndSecret"); // remember me check box enabled.
http.csrf().disable(); // ADD THIS CODE TO DISABLE CSRF IN PROJECT.**
}
Above code:
http.csrf().disable();
will solve the problem.
I have an application with only REST endpoints. I have enabled oauth2 token security via:
#Configuration
#EnableAuthorizationServer
public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("xxx").secret("xxx").accessTokenValiditySeconds(3600)
.authorizedGrantTypes("client_credentials")
.scopes("xxx", "xxx")
.and()
.withClient("xxx").secret("xxx").accessTokenValiditySeconds(3600)
.authorizedGrantTypes("password", "refresh_token")
.scopes("xxx", "xxx");
}
}
Now if I try to access any of my endpoints I get 401 Unauthorized, and I first have to get the access_token via the /oauth/token?grant_type=client_credentials or /oauth/token?grant_type=password calls. The REST endpoints work as expected if I add the proper Authorization header with the token returned in previous call.
However, I am unable to access the swagger-ui page. I have enabled swagger via:
#Configuration
#EnableSwagger2
public class SwaggerConfig {
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select().apis(RequestHandlerSelectors.basePackage("com.xxx"))
.paths(PathSelectors.regex("/xxx/.*"))
.build();
}
}
If I go to localhost:8080/swagger-ui.html I get:
<oauth>
<error_description>
Full authentication is required to access this resource
</error_description>
<error>unauthorized</error>
</oauth>
So I added the following to be able to access Swagger:
#Configuration
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/swagger-ui.html")
.antMatchers("/webjars/springfox-swagger-ui/**")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
And in #EnableWebMvc class I added:
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
Now I can access the Swagger UI page, but my security for the REST endpoints is messed up. By that I mean, the client_credentials endpoints no longer require a token, and the password endpoints give a 403 Forbidden no matter what I do.
I think my approach is wrong but I don't know what. Basically I want:
Oauth token security on all my REST endpoints (beginning with /api/* for example)
Swagger UI page should be accessible
The endpoints on the swagger page should have a way to specify the access_token
How do I achieve this?
This is how I fixed it. I removed the class that extends WebSecurityConfigurerAdapter (see above) and replaced with this:
#Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/xxx/**").authenticated();
http.authorizeRequests().anyRequest().permitAll();
http.csrf().disable();
}
}
To enable token authentication on the swagger page I followed this tutorial: http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
I'm trying to set up a RESTful API with Spring Boot and I'm trying to enable basic authentication. How come I keep hitting a 403 Access Denied error? I'm sending my credentials as a header in Postman (see image attached). If I remove .anyRequest.authenticated(), it works fine. I don't want to remove that though because I would like basic authentication for every endpoint. What am I doing wrong?
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
SecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/h2-console/*").permitAll()
.anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
Controller.java
#RestController
public class Controller {
#RequestMapping("/test")
public String index() {
return "Greetings from Spring Boot!";
}
}
After digging around in the Spring docs, it seems I understand what each of the chained method calls are for.
Anyway, the simple answer is that I needed .and().httpBasic() to enable Basic HTTP Authentication over my REST API.
.anyRequest().authenticated() simply mandates that every request is authenticated, but did not specify what method. Adding basic authentication means we can use basic auth to authenticate a user.
See more.
My application serves both API and browser. I've implemented API Token authentication with all custom providers and filter. The configuration now seems to interfere with the browser version.
I have two questions that I need advice on how to solve, as I'm not getting anywhere after digging through the documentation and other examples.
1) My StatelessAuthenticationFilter is being called despite a request
coming from the browser. I have e.g. specified the request matcher to "/api/**". Why is that?
2) The AuthenticationManager have not registered two AuthenticationProviders. This is my conclusion after debugging my StatelessAuthenticationFilter that's being called wrongly.
Here's the configuration classes that I have
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Order(1)
#Configuration
public static class A extends WebSecurityConfigurerAdapter {
#Autowired
TokenAuthenticationProvider tokenAuthenticationProvider;
#Autowired
ApiEntryPoint apiEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
StatelessAuthenticationFilter filter = new StatelessAuthenticationFilter();
AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher("/api/**");
filter.setRequiresAuthenticationRequestMatcher(requestMatcher);
filter.setAuthenticationManager(super.authenticationManager());
http.csrf().disable()
.exceptionHandling().authenticationEntryPoint(apiEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/user/register");
}
}
#Configuration
public static class B extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new DaoAuthenticationProvider());
}
}
}
As you can see, B class doesn't specify anything, yet when I access localhost:8080 the StatelessAuthenticationFilter is called. What is going on here?
In class A you are configuring the StatelessAuthenticationFilter to use a requestMatcher. Whatever you do with that, spring does not know or care about that.
You must also restrict your security configuration using
http.antMatcher("/api/**")
otherwise its configured for every URI and the StatelessAuthenticationFilter will be invoked for every request, exactly as you described.
You should also annotate class A and B with #Order as shown in the example at multiple-httpsecurity
I had an interesting situation not long ago which caused an infinite loop (and eventually a stack overflow) in Spring Security's AuthenticationManager. For months, everything worked as expected, but then I decided to transfer my XML configuration to code-only configuration. Here was my basic setup in Java configuration:
#Configuration
#EnableWebMvcSecurity
#ComponentScan(basePackages = { "com.my.company" })
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Disable default configuration
public SecurityConfig() {
super(true);
}
#Autowired
AuthenticationProviderImpl authenticationProvider;
#Autowired
MyAuthenticationEntryPoint customAuthenticationEntryPoint;
#Autowired
AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter;
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) throws Exception {
// Ignore requests of resources in security
web.ignoring().antMatchers("/resources/**")
// Ignore requests to authentication
.and().ignoring().antMatchers("/auth/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Define main authentication filter
http.addFilterBefore(authenticationTokenProcessingFilter,
UsernamePasswordAuthenticationFilter.class)
// Request path authorization
.authorizeRequests()
.antMatchers("/api/**")
.access("isAuthenticated()")
// Authentication provider
.and()
.authenticationProvider(authenticationProvider)
// Security failure exception handling
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
// Session Management
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// Default security HTTP headers
.and().headers().xssProtection().frameOptions()
.cacheControl().contentTypeOptions();
}
}
However, I soon found out that this configuration causes issues with my AuthenticationProviderImpl (which implements the Spring Security AuthenticationProvider interface). When the implementation's overridden authenticate method throws a BadCredentialsException, the exact same method in that class is called again perpetually until the stack overflows. The good news is that I fixed my configuration by simply overriding configure(AuthenticationManagerBuilder builder) in the SecurityConfig and declaring my implementation of the AuthenticationProvider there instead of in configure(HttpSecurity http). Here is the fixed version:
#Configuration
#EnableWebMvcSecurity
#ComponentScan(basePackages = { "com.my.company" })
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Disable default configuration
public SecurityConfig() {
super(true);
}
#Autowired
AuthenticationProviderImpl authenticationProvider;
#Autowired
MyAuthenticationEntryPoint customAuthenticationEntryPoint;
#Autowired
AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter;
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(AuthenticationManagerBuilder builder) {
// Configure the authentication manager WITH the authentication
// provider. Not overriding this method causes very bad things to
// happen.
builder.authenticationProvider(authenticationProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
// Ignore requests of resources in security
web.ignoring().antMatchers("/resources/**")
// Ignore requests to authentication
.and().ignoring().antMatchers("/auth/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// Define main authentication filter
http.addFilterBefore(authenticationTokenProcessingFilter,
UsernamePasswordAuthenticationFilter.class)
// Request path authorization
.authorizeRequests()
.antMatchers("/api/**")
.access("isAuthenticated()")
.and()
// Security failure exception handling
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
// Session Management
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// Default security HTTP headers
.and().headers().xssProtection().frameOptions()
.cacheControl().contentTypeOptions();
}
}
Though I believe my problem is solved with the fixed configuration, I still have no idea why the application was infinitely calling authenticate() when an exception was thrown by my implementation of AuthenticationProvider? I tried stepping through and examining the Spring Security classes, but I was not finding a logical answer. Thanks ahead for your expertise!
A few weeks ago I reproduced this behavior, too, see this thread on stackoverflow.
Dealing with the question I figured out that loops occur when the AuthenticationManager internally iterates through it's list of associated AuthenticationProviders, then finds a custom provider and tries to do the authentication using the provider that has been found. If the provider delegates the authentication back to the AuthenticationManager by calling authenticate(), you are in the loop. I guess your AuthenticationProviderImpl does something like that?
The order of your in the providers inside the java.util.List of the AuthenticationManager matters. The order is given by your configuration, e.g. by doing what you tried at first:
// Authentication provider
.and()
.authenticationProvider(authenticationProvider)
By changing your configuration, you influenced the internally managed list of providers attached to your manager, which in the end will solve your code.