With the release of Spring Security 5, one of the new features is the WebFlux for securing reactive applications. In this article, we will be discussing about securing REST endpoints exposed through reactive applications.
At first, we will make configuration to use basic authentication httpBasic()
to secure the reactive REST endpoints and then in the next article we have extended this example to provide token-based custom authentication using JWT. The authorization process will be role-based and we will be using method based reactive security using @PreAuthorize
.
To secure non-reactive endpoints, you can visit my another article here.
You can visit Spring Security Tutorial for all the list of tutorials.
What is Basic Authentication
Basic authentication is a standard HTTP header with the user and password encoded in base64 : Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==. The username and password is encoded in the format username:password. This is one of the simplest technique to protect the REST resources because it does not require cookies. session identifiers or any login pages.
Project Setup
Head over to Spring Initiliazer to download a sample spring boot application with below artifacts. We will be using spring boot 2.1.5.RELEASE.
Below is the pom file.
pom.xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Spring Boot Web Flux Security Configuration
We have defined SecurityWebFilterChain and MapReactiveUserDetailsService bean to configure a basic flux security. The class must be annotated with @EnableWebFluxSecurity
to enable the flux security for a web app. To get started, we have used in-memory user user details for basic authentication.
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { return http .csrf().disable() .authorizeExchange() .pathMatchers("/").permitAll() .anyExchange().authenticated() .and() .httpBasic() .and() .formLogin().disable() .build(); } @Bean public MapReactiveUserDetailsService userDetailsService() { UserDetails user = User.builder() .username("user") .password("{noop}user") .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password("{noop}admin") .roles("ADMIN") .build(); return new MapReactiveUserDetailsService(user, admin); } }
Above configuration allows the context path(/) to be publicly accessible without any authentication whereas other REST endpoints require basic authentication to be accessible.
With httpBasic()
enables the basic authentication
Password is prefixed with {noop}
to indicate to DelegatingPasswordEncoder that NoOpPasswordEncoder should be used.
As we have not explicitly configured role based access, all the secured endpoints is accessible by any role. We will enable method based security in coming sections.
DB Configurations for MapReactiveUserDetailsService
Spring supports reactive repository with Mongo DB. Hence, let us first add the maven dependency to enable it.
You caan follow this article for different ways to configure MongoDB with spring boot.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
You can follow this article to learn about CRUD implementation with Spring Boot and MongoDB.
Now, we can define our repository and implementation of ReactiveUserDetailsService.
UserRepository.javapublic interface UserRepository extends MongoRepository{ Mono findByUsername(String username); }
@Service public class UserDetailsService implements ReactiveUserDetailsService { @Autowired private UserRepository userRepository; @Override public MonoUser.javafindByUsername(String username) { return userRepository.findByUsername(username).switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException("User Not Found")))).map(User::toAuthUser); } }
@Document
public class User {
private String id;
private String username;
private String password;
private String role;
getters and setters
public AuthenticatedUser toAuthUser() {
// returns a AuthenticatedUser object
}
AuthenticatedUser.java
public class AuthenticatedUser implements UserDetails { private String username; private Collection extends GrantedAuthority> authorities; public AuthenticatedUser(String username, Collection extends GrantedAuthority> authorities){ this.username = username; this.authorities = authorities; } @Override public Collection extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return null; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
Reactive REST Endpoints Implementation
Below is the sample controller that exposes 3 REST endpoints each for no auth required, accessible with USER role("/user") and accessible with ADMIN role("/admin").
As we have not explicitly configured role based access, all the secured endpoints is accessible by any role. We will enable method based security in coming sections.
UserController.java@RestController public class UserController { @GetMapping("/") public Monowelcome() { return Mono.just("Welcome !"); } @GetMapping("/user") public Mono greetUser(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Hello, %s", name)); } @GetMapping("/admin") public Mono greetAdmin(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Hello, %s", name)); } }
Now, if we try to access the URL http://localhost:8080/admin without Basic Auth, the response status will be a 401 Unauthorized. The important thing to note here is the default response header added by Spring Security.
Spring Security Default Headers
The default for Spring Security is to include the following headers:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block Referrer-Policy ?no-referrer
Enable Method Level Security
Enabling method level security is extremely easy to configure. We need to annotate our SecurityConfig
class with @EnableReactiveMethodSecurity
and then we can use it at method level in our controller.
Now the endpoint /user
is accessible with user having role USER
.
@PreAuthorize("hasRole('USER')") @GetMapping("/user") public MonogreetUser(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Hello, %s", name)); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin") public Mono greetAdmin(Mono principal) { return principal .map(Principal::getName) .map(name -> String.format("Hello, %s", name)); }
Exception Handling
We can define our custom authentication entry point for exception handling. Below is a sample to handle exceptions with reactive ExceptionhandlingSpec in our SecurityConfig.java under securityFilterChain
...
.and()
.exceptionHandling()
.authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
})).accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
}))
Conclusion
In this example, we created reactive REST endpoints and used spring web flux rest authentication to secure those endpoints. Here, we used basic authentication to secure these endpoints. In the next article, we will create custom token based authentication and authorization using JWT.