4
\$\begingroup\$

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?

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.