I am trying to make an application that uses custom User and Admin credentials for Authentication. However, the custom user credential gets an unauthorized exception. Only I can log in using the system-generated credentials user and generated password.
SecurityConfig.java
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
.formLogin(withDefaults())
.httpBasic(withDefaults());
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
System.out.println("--------------------------------In UserDetailsService");
UserDetails user = User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}}
ApiGatewayApplication,java
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
application.properties
spring.application.name=ApiGateway
server.port=8083
spring.main.allow-bean-definition-overriding=true
logging.level.org.springframework.security=DEBUG
spring.cloud.gateway.routes[0].id=QuestionService
spring.cloud.gateway.routes[0].uri=lb://QuestionService
spring.cloud.gateway.routes[0].predicates[0]=Path=/question/**
spring.cloud.gateway.routes[1].id=QuizService
spring.cloud.gateway.routes[1].uri=lb://QuizService
spring.cloud.gateway.routes[1].predicates[0]=Path=/quiz/**, /quiz-test/**
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gateway</groupId>
<artifactId>ApiGateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ApiGateway</name>
<description>This is a API Gateway Server</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>23</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
API Gateway log
2024-10-10T15:00:11.167-04:00 INFO 21580 --- [ApiGateway] [ main] ctiveUserDetailsServiceAutoConfiguration :
Using generated security password: da26512d-fcb0-49c0-a8e3-5814686ed7a5
After rigorous testing and research, I found out where is the problem:
The problem is that when spring boot application is enabled with spring-cloud-starter-gateway
and spring-boot-starter-security
, then the application is trying to use ReactiveUserDetailsServiceAutoConfiguration
class from security.reactive
package present in the spring-boot-autoconfigure
library to configure reactive based user details service to do the authentication.
That means that your application is enabled with Spring WebFlux by default when spring-cloud-starter-gateway
is used and it has to use that auto-configuration class which is specially made to configure and used to authenticate webflux based application.
Let me demonstrate what I mean:
Scenario 1: When I remove spring-cloud-starter-gateway
& jakarta.servlet-api
dependencies and added the spring-boot-starter-web
dependency, then your configuration works. You can test it out. I was able to login via both user
and admin
. Note: This detail is added just to prove that your configuration is correct.
Scenario 2: I took the same configuration and dependencies as it is that you have provided, then the error that I faced.
org.springframework.security.authentication.BadCredentialsException: Invalid Credentials
at org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager.lambda$authenticate$1(AbstractUserDetailsReactiveAuthenticationManager.java:108) ~[spring-security-core-6.3.3.jar:6.3.3]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:45) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.publisher.Mono.subscribe(Mono.java:4576) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:82) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:182) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.6.10.jar:3.6.10]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.6.10.jar:3.6.10]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
2024-10-13T12:30:27.577+05:30 DEBUG 6790 --- [so-security-demo] [oundedElastic-2] o.s.s.w.s.DefaultServerRedirectStrategy : Redirecting to '/login?error'
Screenshot:
See the log above, it's using flux libraries behind the scenes. This means that your application is enabled with flux when spring-cloud-starter-gateway
and spring-boot-starter-security
library is used.
How to fix this:
Update SecurityConfig
to this:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {
http.authorizeExchange(requests -> requests.anyExchange().authenticated())
.authenticationManager(reactiveAuthenticationManager())
.formLogin(withDefaults())
.httpBasic(withDefaults());
return http.build();
}
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
System.out.println("--------------------------------In UserDetailsService");
UserDetails user = User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}password")
.roles("USER", "ADMIN")
.build();
return new UserDetailsRepositoryReactiveAuthenticationManager(new MapReactiveUserDetailsService(user, admin));
}
}
Points to be noted:
SecurityWebFilterChain
needs to be used for web-flux support. SecurityFilterChain
won't work.
You have to use the ReactiveAuthenticationManager
and also MapReactiveUserDetailsService
designed for reactive based in-memory user details.
Then it will work.
Logs:
2024-10-13T12:40:05.073+05:30 DEBUG 6981 --- [so-security-demo] [ctor-http-nio-2] a.DelegatingReactiveAuthorizationManager : Checking authorization on '/' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@70d82571
2024-10-13T12:40:05.073+05:30 DEBUG 6981 --- [so-security-demo] [ctor-http-nio-2] ebSessionServerSecurityContextRepository : Found SecurityContext 'SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]]' in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@4d1672fd'
2024-10-13T12:40:05.074+05:30 DEBUG 6981 --- [so-security-demo] [ctor-http-nio-2] o.s.s.w.s.a.AuthorizationWebFilter : Authorization successful
Also, your system generated password thing will be solved as well. It won't generate at all.
See if this helps.