Securing Spring WebFlux Reactive APIs with JWT Auth

Securing Spring WebFlux Reactive APIs with JWT Auth thumbnail
67K
By Dhiraj 13 August, 2019

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.

spring-security-webflux-jwt-project-generation

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 RouterFunction userRoute() {
        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 Mono login(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 Mono signUp(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 List authorities = 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 Mono save(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 Mono authenticate(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 ReactiveMongoRepository {

    Mono findByUsername(String username);
}

application.properties
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 List roles;

//setters and getters

}
Role.java
public class Role {

    private String name;
    private String description;

//setters and getters
}

Testing the Application

User Registration

spring-security-webflux-jwt-user-signup

User Login and Token generation

spring-security-webflux-jwt-user-login

Accessing Secured API without JWT Token

spring-webflux-without-jwt-token

Accessing Secured API with JWT Token

spring-webflux-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.

Share

If You Appreciate This, You Can Consider:

We are thankful for your never ending support.

About The Author

author-image
A technology savvy professional with an exceptional capacity to analyze, solve problems and multi-task. Technical expertise in highly scalable distributed systems, self-healing systems, and service-oriented architecture. Technical Skills: Java/J2EE, Spring, Hibernate, Reactive Programming, Microservices, Hystrix, Rest APIs, Java 8, Kafka, Kibana, Elasticsearch, etc.

Further Reading on Spring Security