Greeting to all senior devs here. I'm trying to create a project for my small business. There are not many tutorial on how to create Reactive JWT authentication with Spring webflux. So I'm a bit scared if I make any vulnerabilities. If you could help me review this project I would be grateful. So let's get started.
Security configuration class. I have two difference authentication manager. One for global authentication and one for JWT validation.
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
public class SpringSecurityConfig {
@Autowired
private ReactiveUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ReactiveAuthenticationManager authenticationManager() throws Exception {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http,
JWTTokenService jwtTokenService) throws Exception {
return http.csrf().disable()
.authorizeExchange()
.pathMatchers("/api/v1/user/signin", "/api/v1/user/signup")
.permitAll()
.anyExchange()
.authenticated()
.and()
.addFilterAt(bearerAuthenticationFilter(localAuthenticationManager(), jwtTokenService), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
private AuthenticationWebFilter bearerAuthenticationFilter(ReactiveAuthenticationManager authManager, JWTTokenService jwtTokenService) {
AuthenticationWebFilter bearerAuthenticationFilter = new AuthenticationWebFilter(authManager);
bearerAuthenticationFilter.setServerAuthenticationConverter(new ServerHttpBearerAuthenticationConverter(jwtTokenService));
return bearerAuthenticationFilter;
}
private ReactiveAuthenticationManager localAuthenticationManager() {
return new ReactiveAuthenticationManager () {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.filter(Authentication::isAuthenticated)
.map(auth -> (String) auth.getPrincipal())
.flatMap(username -> userDetailsService.findByUsername(username))
.filter(UserDetails::isEnabled)
.map(user -> authentication)
.switchIfEmpty(Mono.error(new AccessDeniedException("Unauthorized user.")));
}
};
}
}
Server http bearer authentication converter class. It responsible for converting any authenticated request to JWT Token authentication.
public class ServerHttpBearerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String BEARER = "Bearer ";
private static final Predicate<String> matchBearerLength = authValue -> authValue.length() > BEARER.length();
private static final Function<String, Mono<String>> isolateBearerValue = authValue -> Mono.justOrEmpty(authValue.substring(BEARER.length()));
private final JWTTokenService jwtTokenService;
public ServerHttpBearerAuthenticationConverter(JWTTokenService jwtTokenService) {
this.jwtTokenService = jwtTokenService;
}
public static Mono<String> extract(ServerWebExchange serverWebExchange) {
return Mono.justOrEmpty(serverWebExchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION));
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(ServerHttpBearerAuthenticationConverter::extract)
.filter(matchBearerLength)
.flatMap(isolateBearerValue)
.flatMap(jwtTokenService::validateTokenAndGetUsername)
.map(username ->
new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>()));
}
JWT token service. For generate and validate JWT token.
@Service
public class JWTTokenService {
private final Algorithm hmac512;
private final JWTVerifier verifier;
private final int EXPIRATION_TIME = 900_000;
private final String SECRET = "inject_from_env_later";
public JWTTokenService() {
this.hmac512 = Algorithm.HMAC512(SECRET);
this.verifier = JWT.require(this.hmac512).build();
}
public String generateToken(final UserDetails userDetails) {
return JWT.create()
.withSubject(userDetails.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(this.hmac512);
}
public Mono<String> validateTokenAndGetUsername(final String token) {
try {
return Mono.just(verifier.verify(token))
.map(Payload::getSubject);
} catch (final JWTVerificationException verificationEx) {
// log.logwarn("token invalid: {}", verificationEx.getMessage());
return Mono.empty();
}
}
}
UserController for signup and signin method.
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private ReactiveUserDetailsService userDetailsService;
@Autowired
private ReactiveAuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JWTTokenService jwtTokenService;
@PostMapping("/signup")
public Mono<User> signupUser(@RequestBody RequestSignupUser request) {
return userRepository.save(new User(UUID.randomUUID(),
request.getUsername(),
passwordEncoder.encode(request.getPassword())));
}
@PostMapping("/signin")
public Mono<AuthenticationResponse> siginUser(@RequestBody RequestSigninUser request) {
try {
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(),
request.getPassword()))
.flatMap(authen -> userDetailsService.findByUsername(request.getUsername()))
.map(user -> new AuthenticationResponse(jwtTokenService.generateToken(user)));
} catch (final BadCredentialsException ex) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
}
}
I have tested with PostMan several times and it works just fine I think?