In this post, we will see how to integrate Spring Security in a reactive Spring Webflux with MongoDB and Flutter.
Prerequisites
This is the list of all the prerequisites for following the part 1:
- Java 17
- Spring Boot / Starter WebFlux 2.6.7
- Lombok 1.18
- Maven 3.6.3
- Postman
- Mongo 4.4.
Overview
What is a One Time Password (OTP)?
A one-time password (OTP), also known as a one-time PIN, one-time authorization code (OTAC), or dynamic password, is a password that is valid for only one login session or transaction, on a computer system or other digital device. — https://en.wikipedia.org/wiki/One-time_password
What is the benefit of using OTP?
The most important advantage addressed by OTPs is that, in contrast to static passwords, they are not vulnerable to replay attacks. This means that a potential intruder who manages to record an OTP that was already used to log into a service or to conduct a transaction will not be able to use it, since it will no longer be valid.
Getting Started
We will implement token-based authentication and authorization using JSON Web Token (JWT) provider with Spring WebFlux security.
The authentication flow is described by the following diagram:

We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Reactive Web, Spring Security, Spring Data Reactive MongoDB NoSQL, Java Mail Sender, Thymeleaf, Lombok, and Validation.
Below are the maven project dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-core</artifactId>
<version>${springdoc.openapi.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<version>${springdoc.openapi.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${io.jsonwebtoken.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${io.jsonwebtoken.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>compile</scope>
<version>${io.jsonwebtoken.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
First, Let’s create a User POJO class that implements UserDetails. We use the annotation @Document to set the collection name that will be used by the model.
/**
* A user.
*/
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(collection = "app_user")
public class User implements UserDetails {
@Serial
private static final long serialVersionUID = 1L;
@Id
private String id;
@NotNull
@Size(min = 1, max = 50)
@Indexed(unique = true)
private String username;
@Getter(onMethod = @__(@JsonIgnore))
@NotNull
@Size(min = 4, max = 60)
private String password;
@Size(max = 50)
@Field("first_name")
private String firstName;
@Size(max = 50)
@Field("last_name")
private String lastName;
@Email
@Size(min = 5, max = 254)
@Indexed
private String email;
@Field("otp_request")
private OtpRequest otpRequest;
@JsonIgnore
private Set<Role> roles = new HashSet<>();
private boolean enabled = false;
private boolean accountNonExpired;
private boolean credentialsNonExpired;
private boolean accountNonLocked;
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public boolean isAccountNonExpired() {
return !accountNonExpired;
}
@Override
public boolean isCredentialsNonExpired() {
return !credentialsNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return !accountNonLocked;
}
/*
* Get roles and add them as a Set of GrantedAuthority
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toSet());
}
}
Our User class contains the embedded modelsOtpRequest et Role that manage OTP codes and user permissions.
Implement ReactiveUserDetailsService
The ReactiveUserDetailsService defines a method used by Spring Security to retrieve a user by its username that returns a Mono<UserDetails>.
We’ll create a class called UserDetailsService that overrides the method findByUsername() of the ReactiveUserDetailsService interface.
In this method, we retrieve the User object using the UserRepository if it exists.
@Slf4j
@RequiredArgsConstructor
@Component("userDetailsService")
public class UserDetailsService implements ReactiveUserDetailsService{
private final AccountStatusUserDetailsChecker detailsChecker = new AccountStatusUserDetailsChecker();
private final UserRepository userRepository;
@Override
public Mono<UserDetails> findByUsername(final String login) {
LOGGER.debug("Authenticating with {}", login);
String username = StringUtils.trimToNull(login.toLowerCase());
if (new EmailValidator().isValid(username, null)) {
return userRepository
.findOneByEmailIgnoreCase(username)
.switchIfEmpty(Mono.error(new UsernameNotFoundException(MessageFormat.format("User with email {0} was not found.", username))))
.map(this::getUserDetails);
}
return userRepository
.findOneByUsernameIgnoreCase(username)
.switchIfEmpty(Mono.error(new UsernameNotFoundException(MessageFormat.format("User {0} was not found", username))))
.map(this::getUserDetails);
}
private UserDetails getUserDetails(User user) {
detailsChecker.check(user);
return user;
}
}
Configure Spring Security
In the configuration package, create SecurityConfiguration class that will manage all security aspects.
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfiguration {
private final String[] permitAllPatterns = new String[] {"/api/user/login","/api/user/register"};
private final ReactiveUserDetailsService userDetailsService;
private final TokenProvider tokenProvider;
private final CustomAccessDeniedHandler accessDeniedHandler;
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
public SecurityConfiguration(ReactiveUserDetailsService userDetailsService, TokenProvider tokenProvider, CustomAccessDeniedHandler accessDeniedHandler, CustomAuthenticationEntryPoint authenticationEntryPoint) {
this.userDetailsService = userDetailsService;
this.tokenProvider = tokenProvider;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(
userDetailsService
);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.securityMatcher(new NegatedServerWebExchangeMatcher(new OrServerWebExchangeMatcher(
pathMatchers("/webjars/**","/app/**", "/i18n/**", "/content/**", "/swagger-ui/**", "/v3/api-docs/**", "/test/**"),
pathMatchers(HttpMethod.OPTIONS, "/**")
)))
.csrf()
.disable()
// Add JWT token filter
.addFilterAt(new SecurityContextFilter(tokenProvider), SecurityWebFiltersOrder.HTTP_BASIC)
.authenticationManager(reactiveAuthenticationManager())
// Set unauthorized requests exception handler
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
.headers()
.contentSecurityPolicy(AppConstant.DEFAULT_SRC_SELF_POLICY)
.and()
.referrerPolicy(ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.permissionsPolicy().policy(AppConstant.PERMISSION_POLICY)
.and()
.frameOptions().mode(Mode.DENY)
.and()
// Set permissions on endpoints
.authorizeExchange()
.pathMatchers(permitAllPatterns).permitAll()
.pathMatchers("/api/user/**").authenticated();
// @formatter:on
return http.build();
}
}
Let me explain the code above.
– @EnableWebFluxSecurity allows WebFlux support in Spring Security.
– @EnableReactiveMethodSecurityallows method security supports in a reactive application and then uses method-level annotations, such as @PreAuthorize(“isAuthenticated()”)
– passwordEncoder()bean method for encoding passwords
– reactiveauthenticationManager()bean method that uses a ReactiveUserDetailsService to validate the provided username and password.
– springSecurityFilterChain(ServerHttpSecurity http) bean method with ServerHttpSecurity as the parameter. ServerHttpSecurity is similar to Spring Security’s HttpSecurity but for WebFlux. It allows configuring web-based security for specific HTTP requests.
WebFlux Handler and Router Functions
Now, let’s implement the router and manager functions. First, we will create the AccountHandler class which contains login(), register(), otpCheckCode(), otpResendCode()methods.
@Component
@RequiredArgsConstructor
public class AccountHandler {
private final UserService userService;
private final Validator validator;
private final ReactiveAuthenticationManager authenticationManager;
@PreAuthorize("hasRole('ROLE_USER') AND hasRole('ROLE_ADMIN')")
public Mono<ServerResponse> isAuthenticated(ServerRequest serverRequest) {
return serverRequest.principal()
.map(Principal::getName)
.flatMap(user ->
ServerResponse.status(HttpStatus.OK)
.bodyValue(new ApiResponseDTO(user, "Current user is authenticated")));
}
@PreAuthorize("hasRole('PRE_AUTH')")
public Mono<ServerResponse> optCheckCode(ServerRequest serverRequest) {
var otpCode = serverRequest.pathVariable("code");
return serverRequest.principal()
.map(Principal::getName)
.flatMap(u -> userService.checkCode(u, otpCode))
.flatMap(token ->
ServerResponse.status(HttpStatus.OK)
.bodyValue(new ApiResponseDTO(token, "Otp checking success")));
}
@PreAuthorize("hasRole('PRE_AUTH')")
public Mono<ServerResponse> optResendCode(ServerRequest serverRequest) {
return serverRequest.principal()
.map(Principal::getName)
.flatMap(userService::resendCode)
.flatMap(token ->
ServerResponse.status(HttpStatus.OK)
.bodyValue(new ApiResponseDTO(token, "Otp checking success")));
}
public Mono<ServerResponse> login(final ServerRequest request) {
return request.bodyToMono(LoginDTO.class)
.flatMap(body ->
validator.validate(body).isEmpty()
? Mono.just(body)
: Mono.error(new ValidatorException(validator.validate(body).stream().map(ConstraintViolation::getMessage).toList().toString()))
)
.flatMap(login ->
authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword()))
.flatMap(userService::setUserOtp)
)
.flatMap(jwt ->
ServerResponse.status(HttpStatus.OK)
.bodyValue(new ApiResponseDTO(jwt.token(), "User login success")));
}
public Mono<ServerResponse> register(final ServerRequest request) {
return request.bodyToMono(UserPasswordDTO.class)
.flatMap(body ->
validator.validate(body).isEmpty()
? Mono.just(body)
: Mono.error(new ValidatorException(validator.validate(body).stream().map(ConstraintViolation::getMessage).toList().toString()))
)
.flatMap(userService::createUser)
.flatMap(savedUser ->
ServerResponse.status(HttpStatus.CREATED)
.bodyValue(new ApiResponseDTO(savedUser, "User created successfully")));
}
}
The handle functional method accepts ServerRequest and returns Mono<ServerResponse>.
Incoming requests are routed to a handler function with a RouterFunction.
@Configuration
@AllArgsConstructor
public class AccountRouter {
@RouterOperations({
@RouterOperation(path = "/api/user/authenticate", produces = {MediaType.APPLICATION_JSON_VALUE },
beanClass = AccountHandler.class, beanMethod = "isAuthenticated",
operation = @Operation(operationId = "isAuthenticated",
responses = {
@ApiResponse(responseCode = "200", description = "get current user.", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class)))
})
),
@RouterOperation(path = "/api/user/register", produces = {MediaType.APPLICATION_JSON_VALUE },
beanClass = AccountHandler.class, beanMethod = "register",
operation = @Operation(operationId = "register",
responses = {
@ApiResponse(responseCode = "201", description = "User account created.", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "404", description = "User account already exist", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "400", description = "Bad request", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "500", description = "Something went wrong", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
}, requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = UserPasswordDTO.class )))
)
),
@RouterOperation(path = "/api/user/login", produces = {MediaType.APPLICATION_JSON_VALUE },
beanClass = AccountHandler.class, beanMethod = "login",
operation = @Operation(operationId = "login",
responses = {
@ApiResponse(responseCode = "200", description = "User account created.", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "404", description = "User account already exist", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "400", description = "Bad request", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "500", description = "Something went wrong", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
}, requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = LoginDTO.class )))
)
),
@RouterOperation(path = "/api/user/otp/{code}", produces = {MediaType.APPLICATION_JSON_VALUE },
beanClass = AccountHandler.class, beanMethod = "optCheckCode",
operation = @Operation(operationId = "optCheckCode",
responses = {
@ApiResponse(responseCode = "200", description = "OTP success.", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "404", description = "User account not found", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "400", description = "Bad request", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class))),
@ApiResponse(responseCode = "500", description = "Something went wrong", content = @Content(schema = @Schema(implementation = ApiResponseDTO.class)))
}, parameters = {@Parameter(in = ParameterIn.PATH, name = "code") }
)
),
})
@Bean
public RouterFunction<ServerResponse> routeUserAccount(final AccountHandler accountHandler) {
return route()
.nest(path("/api/user"), builder ->
builder
.GET("/authenticate", accountHandler::isAuthenticated)
.GET("/resend/code", accountHandler::optResendCode)
.POST("/register", accountHandler::register)
.POST("/login", accountHandler::login)
.GET("/otp/{code}", accountHandler::optCheckCode))
.build();
}
}
Run & Test
Run the app.
Open http://localhost:8080/webjars/swagger-ui/index.html URL in a web browser.

It works so far 👌
The first step is to create a new user.

We log in with the username and password of the user created previously.

When the information is correct, we will create a temporary token (10 min) with the following message: Partially successful user login — an OTP code has been sent to your email address
The content of the email is as follows:

Continue reading
In the second part, we implemented the whole process with a mobile application using Flutter and Dart.
The complete backend source code can be found in my GitHub repository.
If you enjoyed this article, please give it a few claps for support.
Happy coding 🙂.