How to define 3 authentication ways config - httpBasic, userDetailsService, JsonLoginFilter()

I'm creating an app where a user can authenticate using three ways httpBasic, userDetailsService, JsonLoginFilter(). A user should be authenticated using Spring's default /login path. But the type is one of three: httpBasic: There must be 2 users in memory database for testing. Simple user with user role and admin user with its role. userDetailsService: User and Role entities are read or saved to MySQL database. Authentication is possible when using UserDetailsService implementation bean. JsonLoginFilter(): A user sends a request body but not as x-www-form-urlencoded or form-data. The body is raw JSON consisting of a pair <key>: <value> entries. Username and password. And JsonLoginFilter() is the filter that intercepts it. This is my configuration:
12 Replies
JavaBot
JavaBot2mo ago
This post has been reserved for your question.
Hey @Tomasm21! Please use /close or the Close Post button above when your problem is solved. Please remember to follow the help guidelines. This post will be automatically marked as dormant after 300 minutes of inactivity.
TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.
Tomasm21
Tomasm21OP2mo ago
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Autowired
private UserDetailsServicImp userDetailsService;

@Autowired
private CustomAuthenticationFailureHandler customFailureHandler;

@Autowired
private EncoderConfig passwordEncoder;

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/hi", true)
.failureHandler(customFailureHandler)
//.failureUrl("/login?error=true")
.permitAll())
.httpBasic(Customizer.withDefaults())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Logout successful\"}");
})
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll())
.addFilterBefore(new JsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.authenticationManager(authenticationManager());
return httpSecurity.build();
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Autowired
private UserDetailsServicImp userDetailsService;

@Autowired
private CustomAuthenticationFailureHandler customFailureHandler;

@Autowired
private EncoderConfig passwordEncoder;

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin
.loginPage("/login")
.defaultSuccessUrl("/hi", true)
.failureHandler(customFailureHandler)
//.failureUrl("/login?error=true")
.permitAll())
.httpBasic(Customizer.withDefaults())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Logout successful\"}");
})
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll())
.addFilterBefore(new JsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.authenticationManager(authenticationManager());
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider inMemoryProvider = new DaoAuthenticationProvider();
inMemoryProvider.setUserDetailsService(inMemoryUserDetailsManager());
inMemoryProvider.setPasswordEncoder(passwordEncoder.passwordEncoder());

DaoAuthenticationProvider databaseProvider = new DaoAuthenticationProvider();
databaseProvider.setUserDetailsService(userDetailsService);
databaseProvider.setPasswordEncoder(passwordEncoder.passwordEncoder());

// Combine both providers in a custom AuthenticationManager
return new ProviderManager(List.of(inMemoryProvider, databaseProvider));
}

@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
// Create two in-memory users
UserDetails user = User.withUsername("user")
.password(passwordEncoder.passwordEncoder().encode("userpass"))
.roles("USER")
.build();

UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.passwordEncoder().encode("adminpass"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider inMemoryProvider = new DaoAuthenticationProvider();
inMemoryProvider.setUserDetailsService(inMemoryUserDetailsManager());
inMemoryProvider.setPasswordEncoder(passwordEncoder.passwordEncoder());

DaoAuthenticationProvider databaseProvider = new DaoAuthenticationProvider();
databaseProvider.setUserDetailsService(userDetailsService);
databaseProvider.setPasswordEncoder(passwordEncoder.passwordEncoder());

// Combine both providers in a custom AuthenticationManager
return new ProviderManager(List.of(inMemoryProvider, databaseProvider));
}

@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
// Create two in-memory users
UserDetails user = User.withUsername("user")
.password(passwordEncoder.passwordEncoder().encode("userpass"))
.roles("USER")
.build();

UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.passwordEncoder().encode("adminpass"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
It executes well but in console I get:
Tomasm21
Tomasm21OP2mo ago
Problem:
Found 2 UserDetailsService beans, with names [userDetailsServicImp, inMemoryUserDetailsManager]. Global Authentication Manager will not use a UserDetailsService for username/password login. Consider publishing a single UserDetailsService bean.
Found 2 UserDetailsService beans, with names [userDetailsServicImp, inMemoryUserDetailsManager]. Global Authentication Manager will not use a UserDetailsService for username/password login. Consider publishing a single UserDetailsService bean.
and
No authenticationProviders and no parentAuthenticationManager defined. Returning null.
No authenticationProviders and no parentAuthenticationManager defined. Returning null.
As a result I can't authenticate in either way. I get an exception:
Tomasm21
Tomasm21OP2mo ago
I want to have all three ways to authenticate. Did I defined it wrong in my SecurityConfiguration ? How should it be defined? Why Spring doesn't accept my changed AuthenticationManager with two DaoAuthenticationProvider providers that each provides different way to authenticate? One for in memory db and another using real MySql db. JsonLoginFilter():
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType() != null && request.getContentType().equals("application/json")) {
try {
// Parse the raw JSON request body
ObjectMapper objectMapper = new ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, String> credentials = objectMapper.readValue(request.getInputStream(), Map.class);
String username = credentials.get("username");
String password = credentials.get("password");

// Create the authentication token with the username and password
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new BadCredentialsException("Failed to parse JSON request", e);
}
}

// Fall back to the default form login behavior
return super.attemptAuthentication(request, response);
}
}
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType() != null && request.getContentType().equals("application/json")) {
try {
// Parse the raw JSON request body
ObjectMapper objectMapper = new ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, String> credentials = objectMapper.readValue(request.getInputStream(), Map.class);
String username = credentials.get("username");
String password = credentials.get("password");

// Create the authentication token with the username and password
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new BadCredentialsException("Failed to parse JSON request", e);
}
}

// Fall back to the default form login behavior
return super.attemptAuthentication(request, response);
}
}
red
red2mo ago
Try to create two AuthenticationProvider, one for each UserDetailsService and then register both of them with @Bean I think that this way you can separate the two forms of login and still delegate the management of beans to Spring Security as it should be. Or something like that Name the beans and then use @Qualifier to specify which authenticationProvider you want to inject
Tomasm21
Tomasm21OP2mo ago
But would it mean that if I will qualify the databaseProvider bean and then in postman I will try to autheticate using Basic Auth then what happens next? Due to qualifiers it will always try to athenticate me using databaseProvider instead of the inMemoryProvider. And perhaps I won't be able to auntheticate. There should be a way. These problems are not new.
red
red2mo ago
Yes, indeed. But you can get around this by creating two SecurityFilterChain and specifying which authenticationProvider to use.
Tomasm21
Tomasm21OP2mo ago
Ok I will try
red
red2mo ago
The problem with this approach that I mentioned is that it is not possible for the same route to be accessed by more than one form of authentication. For more flexibility, you also should create a filter that processes the user credentials, creates an Authentication object and then based on the obtained data, delegate the authentication to an AuthenticationProvider But maybe this might not be a problem in your case
JavaBot
JavaBot2mo ago
💤 Post marked as dormant
This post has been inactive for over 300 minutes, thus, it has been archived. If your question was not answered yet, feel free to re-open this post or create a new one. In case your post is not getting any attention, you can try to use /help ping. Warning: abusing this will result in moderative actions taken against you.

Did you find this page helpful?