In this article, we will learn about securing reactive REST endpoints with spring Webflux security. We will implement token-based authentication and authorization using JWT provider.
In addition, we will have REST endpoints for user login and registration too. We do not require any token to access these APIs but all the other APIs to perform different CRUD operations will be secured one. We will have Mongo DB integrated with it to store user details and respective roles to configure a role-based authorization system. All the endpoints in this article will be the functional endpoints and we will be using ReactiveMongoRepository to support reactive repository.
Before getting started with this article, let us summarise the different tutorials that we have covered so far under spring Webflux.
Reactive REST Endpoints with Spring Webflux(Both functional and traditional style) REST Basic Authentication with Spring Webflux An API Gateway Implementation with Spring Webflux
Project Overview
There are basically 3 different layers as Security Filter Layer, Handler Function layer, DAO layer.
Handler Function Layer as AuthHandler.java and UserHandler.java. AuthHandler has implementation for user login and user signup. A user login is verified from Mongo DB and after a successfull login, JWT token is returned. Userhandler has impelementation for secured APIs such as user creation, deletion, etc.
DAO Layer implements ReactiveMongoRepository for different reactive DB operations.
Security Filter layer validates the JWT token from the header and sets the security context after successfull validation.
We have BeanConfig.java that has all the functional endpoints defined. Similarly, we have TokenProvider.java that has util methods related to JWT token generation and validation.
Project Setup
Head over to start.spring.io and download a sample spring boot project with spring reactive web, security starter and reactive mongo and import into your workspace.
Below is the maven dependency in case if you have an existing project.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</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-data-mongodb-reactive</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
WebFlux Router Function
Our router functions are defined in BeanConfig.java . We have 2 different routes defined for user and authentication. This acts as an entry point to Spring's functional web framework.
Router function has all the route mappings for our APIs for different HTTP methods similar to @RequestMapping and upon the match of a route, the request is then forwarded to a matching handler function that handles the request for further processing.
Router Function represents a function that routes the request to a Handler function. It is a function that takes a ServerRequest as input and returns a Mono. ServerRequest and ServerResponse are immutable interfaces. If a request matches a particular route, a handler function is returned and otherwise, it returns an empty Mono.
In the below implementation, we have all the routes defined for User to perform different HTTP operations such as POST, GET, PUT and DELETE. On the match of the predicate, the corresponding handler is executed.
@Configuration public class BeanConfig { @Autowired private UserRepository userRepository; @Autowired private AuthHandler authHandler; @Bean public RouterFunctionuserRoute() { UserHandler userHandler = new UserHandler(userRepository); return RouterFunctions .route(POST("/users").and(accept(APPLICATION_JSON)), userHandler::createUser) .andRoute(GET("/users").and(accept(APPLICATION_JSON)), userHandler::listUser) .andRoute(GET("/users/{userId}").and(accept(APPLICATION_JSON)), userHandler::getUserById) .andRoute(PUT("/users").and(accept(APPLICATION_JSON)), userHandler::createUser) .andRoute(DELETE("/users/userId").and(accept(APPLICATION_JSON)), userHandler::deleteUser); } @Bean public RouterFunction authRoute() { return RouterFunctions .route(POST("/auth/login").and(accept(APPLICATION_JSON)), authHandler::login) .andRoute(POST("/auth/signup").and(accept(APPLICATION_JSON)), authHandler::signUp); } }
Web flux Handler Function
We have 2 handler function implementation each for User and Auth. The user handler function is exactly the same as we defined in our last example. Here, is the github link for that handler function.
Below is the implementation of our AuthHandler.java
User Login
The login function fetches user from DB by supplied username and compares the password. If the password is matched then a JWT token is created and the server response is returned.
We have specific validations for user does not exist and invalid credentials.
We have used BodyInserters to generate our response body and BCryptPasswordEncoder for password hashing.
public Monologin(ServerRequest request) { Mono loginRequest = request.bodyToMono(LoginRequest.class); return loginRequest.flatMap(login -> userRepository.findByUsername(login.getUsername()) .flatMap(user -> { if (passwordEncoder.matches(login.getPassword(), user.getPassword())) { return ServerResponse.ok().contentType(APPLICATION_JSON).body(BodyInserters.fromObject(new LoginResponse(tokenProvider.generateToken(user)))); } else { return ServerResponse.badRequest().body(BodyInserters.fromObject(new ApiResponse(400, "Invalid credentials", null))); } }).switchIfEmpty(ServerResponse.badRequest().body(BodyInserters.fromObject(new ApiResponse(400, "User does not exist", null))))); }
User Registration
The registration process validates for any duplicate username in the DB and creates a user entry in the DB after validation. For a new user creation, the password is first bcrypted and then saved to DB.
public MonosignUp(ServerRequest request) { Mono userMono = request.bodyToMono(User.class); return userMono.map(user -> { user.setPassword(passwordEncoder.encode(user.getPassword())); return user; }).flatMap(user -> userRepository.findByUsername(user.getUsername()) .flatMap(dbUser -> ServerResponse.badRequest().body(BodyInserters.fromObject(new ApiResponse(400, "User already exist", null)))) .switchIfEmpty(userRepository.save(user).flatMap(savedUser -> ServerResponse.ok().contentType(APPLICATION_JSON).body(BodyInserters.fromObject(savedUser))))); }
JWT Token Util
There is nothing new that we have implemented related to WebFlux for JWT token generation util class. It is the same old class that we defined here in our last example.
Below is the snippet that generates the JWT token. It adds the username as the JWT subject and adds user roles in the custom claims.
public String generateToken(User user) { final Listauthorities = user.getRoles().stream() .map(Role::getName) .collect(Collectors.toList()); return Jwts.builder() .setSubject(user.getUsername()) .claim(AUTHORITIES_KEY, authorities) .signWith(SignatureAlgorithm.HS256, SIGNING_KEY) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000)) .compact(); }
Spring Webflux Security Configuration
Below is our web flux security configuration. The class must be annotated with @EnableWebFluxSecurity to enable the flux security for a web app.
If you see the configuration, the endpoint /auth
is permitted to access without any token where as all the REST endpoints are secured. The security exception handling is also configued here. We will implement our authentication manager and security context in our next section.
SecurityContextRepository is similar to userDetailsService provided in regular spring security that compares the username and password of the user.
@Configuration @EnableWebFluxSecurity public class SecurityConfig{ @Autowired private AuthenticationManager authenticationManager; @Autowired private SecurityContextRepository securityContextRepository; @Bean SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { String[] patterns = new String[] {"/auth/**"}; return http.cors().disable() .exceptionHandling() .authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() -> { swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); })).accessDeniedHandler((swe, e) -> Mono.fromRunnable(() -> { swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN); })).and() .csrf().disable() .authenticationManager(authenticationManager) .securityContextRepository(securityContextRepository) .authorizeExchange() .pathMatchers(patterns).permitAll() .pathMatchers(HttpMethod.OPTIONS).permitAll() .anyExchange().authenticated() .and() .build(); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Webflux Security Context Repository
The implementation extracts JWT token from the header if present and invokes authenticate()
that actually decodes the username and claims(in our case user roles) and sets the spring security context.
@Component public class SecurityContextRepository implements ServerSecurityContextRepository{ private static final Logger logger = LoggerFactory.getLogger(SecurityContextRepository.class); private static final String TOKEN_PREFIX = "Bearer "; @Autowired private AuthenticationManager authenticationManager; @Override public Monosave(ServerWebExchange swe, SecurityContext sc) { throw new UnsupportedOperationException("Not supported yet."); } @Override public Mono load(ServerWebExchange swe) { ServerHttpRequest request = swe.getRequest(); String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); String authToken = null; if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) { authToken = authHeader.replace(TOKEN_PREFIX, ""); }else { logger.warn("couldn't find bearer string, will ignore the header."); } if (authToken != null) { Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken); return this.authenticationManager.authenticate(auth).map((authentication) -> new SecurityContextImpl(authentication)); } else { return Mono.empty(); } } }
Spring Security Authentication Manager
The class implements ReactiveAuthenticationManager and overrides authenticate
method. The method does all the token related decoding for username and roles.
@Component public class AuthenticationManager implements ReactiveAuthenticationManager { @Autowired private TokenProvider tokenProvider; @Override @SuppressWarnings("unchecked") public Monoauthenticate(Authentication authentication) { String authToken = authentication.getCredentials().toString(); String username; try { username = tokenProvider.getUsernameFromToken(authToken); } catch (Exception e) { username = null; } if (username != null && ! tokenProvider.isTokenExpired(authToken)) { Claims claims = tokenProvider.getAllClaimsFromToken(authToken); List roles = claims.get(AUTHORITIES_KEY, List.class); List authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList()); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, username, authorities); SecurityContextHolder.getContext().setAuthentication(new AuthenticatedUser(username, authorities)); return Mono.just(auth); } else { return Mono.empty(); } } }
Reactive MongoDB Configuration
Our UserRepository implements ReactiveMongoRepository to utilise spring data JPA features with MongoDB. You can follow this article to learn more about spring weblux with MongoDB or this article to configure MongoDB with spring Boot.
public interface UserRepository extends ReactiveMongoRepositoryapplication.properties{ Mono findByUsername(String username); }
spring.data.mongodb.uri=mongodb://root:root@localhost:27017/test_db
This is all about the spring webflux with JWT. Below are some of the model classes and DTOs that we have used in this example.
Model Class Implementation
User.java@Document public class User { @Id private String id; private String name; private int age; private double salary; @Indexed private String username; private String password; private ListRole.javaroles; //setters and getters }
public class Role { private String name; private String description; //setters and getters }
Testing the Application
User Registration
User Login and Token generation
Accessing Secured API without JWT Token
Accessing Secured API with JWT Token
Conclusion
In this article, we learnt about securing reactive REST endpoints with spring webflux security and implemented token-based authentication and authorization with JWT provider.