Introduction

Summary: I’m documenting this so that I can refer back to it in the future. Recently, I’ve been working with Spring WebFlux, and although I’ve previously used JWTs, this is my first time writing about them. I often forget the implementation details when switching between languages or frameworks, so I’m recording this while it’s still fresh in my mind.

JSON Web Tokens (JWTs) are a compact way to prove a user’s identity between two parties—usually a browser and your API.


Token life-cycle

A JWT is self-contained: it carries the user’s ID, roles, and an expiry inside the token itself.
To balance security and convenience I use two clocks:

ClockWhat it limitsTypical valueWhat happens when it hits zero
Access expiryHow long the current token stays valid15–30 minClient must call /auth/refresh
Refresh expiryHow long the refresh token can mint new access tokens7–30 daysUser must log in again

Flow:

/login  ──► access + refresh token
         │
   (before access expiry)
         ▼
/refresh ──► new access token

After the refresh expiry passes, even the refresh endpoint refuses to issue new tokens—forcing a fresh login.


Building blocks

  1. Model – database tables for users & login history
  2. Repository – Reactive CRUD interfaces provided by Spring Data R2DBC
  3. Service – business logic (password checks, audit log)
  4. Auth manager – validates credentials or JWT claims
  5. Filter – extracts the Authorization: Bearer … header
  6. Security config – wires it all together

Essential code

Entity Classes

These classes represent database tables as Java objects using Spring Data R2DBC. @Table maps the class to a table, and Lombok annotations (@Data, @Builder, etc.) generate getters, setters, constructors, and a builder.

@Table("users")
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class UserEntity {
  @Id  private Long id;
  private String username;
  private String password;   // hashed
  private String roles;      // e.g. "USER,ADMIN"
  private boolean enabled;
}

@Table("login_history")
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class LoginHistory {
  @Id  private Long id;
  private Long userId;
  private LocalDateTime loginTime;
  private String ipAddress;
  private boolean successful;
}

Repositories

Spring Data’s ReactiveCrudRepository provides reactive CRUD operations. By extending these interfaces, Spring auto-generates the implementation at runtime.

public interface UserRepository
    extends ReactiveCrudRepository<UserEntity, Long> {
  Mono<UserEntity> findByUsername(String username);
}

public interface LoginHistoryRepository
    extends ReactiveCrudRepository<LoginHistory, Long> {}

JWT filter (excerpt)

ServerAuthenticationConverter is a Spring Security WebFlux interface for converting HTTP requests into Authentication objects. In this converter, we extract and validate the JWT token from the Authorization header.

@Slf4j
public class JwtAuthConverter implements ServerAuthenticationConverter {
  @Override
  public Mono<Authentication> convert(ServerWebExchange exchange) {
    String header = exchange.getRequest()
                            .getHeaders()
                            .getFirst(HttpHeaders.AUTHORIZATION);

    if (header == null || !header.startsWith("Bearer ")) return Mono.empty();

    try {
      String token = header.substring(7);
      Jws<Claims> claims = JwtUtil.validateTokenAndGetClaims(token);

      String username = claims.getBody().getSubject();
      List<String> roles = claims.getBody().get("roles", List.class);

      List<GrantedAuthority> authorities = roles.stream()
          .map(SimpleGrantedAuthority::new)
          .collect(Collectors.toList());

      return Mono.just(new UsernamePasswordAuthenticationToken(
          username, null, authorities));

    } catch (Exception ex) {
      log.warn("Invalid token: {}", ex.getMessage());
      return Mono.empty();
    }
  }
}

Security configuration (simplified)

The @Configuration class defines beans for Spring’s application context. @EnableWebFluxSecurity activates WebFlux security support. We configure a SecurityWebFilterChain bean to set up CSRF, CORS, endpoint permissions, and insert our JWT filter.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

  @Bean
  public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

    JwtAuthWebFilter jwt = new JwtAuthWebFilter(new JwtReactiveAuthManager());
    jwt.setServerAuthenticationConverter(new JwtAuthConverter());

    return http
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .cors(ServerHttpSecurity.CorsSpec::disable)
        .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
        .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
        .exceptionHandling(e -> e.authenticationEntryPoint(
            (exch, ex) -> {
              exch.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
              return exch.getResponse().setComplete();
            }))
        .authorizeExchange(ex -> ex
            .pathMatchers(
                "/api/v1/auth/login",
                "/api/v1/auth/refresh",
                "/api/v1/auth/validate",
                "/actuator/**")
            .permitAll()
            .pathMatchers("/api/v1/task/**").authenticated())
        .addFilterAt(jwt, SecurityWebFiltersOrder.AUTHENTICATION)
        .build();
  }
}

Reactive authentication manager

Implementing ReactiveAuthenticationManager allows custom authentication logic in WebFlux. This manager checks credentials, handles disabled accounts, and records login attempts via the UserService.

@Component
@RequiredArgsConstructor
public class DatabaseReactiveAuthManager implements ReactiveAuthenticationManager {

  private final UserService users;

  @Override
  public Mono<Authentication> authenticate(Authentication auth) {
    String username = auth.getName();
    String rawPwd   = auth.getCredentials().toString();

    return users.findByUsername(username)
        .switchIfEmpty(Mono.error(new BadCredentialsException("User not found")))
        .flatMap(u -> {
          if (!users.checkPassword(rawPwd, u.getPassword()))
            return users.recordLoginAttempt(u, false, null)
                        .then(Mono.error(new BadCredentialsException("Bad password")));

          if (!u.isEnabled())
            return Mono.error(new DisabledException("Account disabled"));

          List<GrantedAuthority> roles = Arrays.stream(u.getRoles().split(","))
              .map(String::trim)
              .map(SimpleGrantedAuthority::new)
              .collect(Collectors.toList());

          return users.recordLoginAttempt(u, true, null)
                      .thenReturn(new UsernamePasswordAuthenticationToken(
                          username, null, roles));
        });
  }
}

Configuration properties

These YAML properties configure Spring Security’s JWT support, specifying the signing secret and token lifetimes.

security:
  encryption:
    secret: _"encryption_secret"_
  jwt:
    expiration-time: __1_440_000__           # 24 minutes (ms)
    refresh-expiration-time: __604_800_000__ # 7 days (ms)

Tip – generate a strong secret:

# This one-off command uses Docker to generate a bcrypt hash for a random 32-byte secret
docker run --rm python:3.9-slim \
  python - <<'PY'
import bcrypt, os
print(bcrypt.hashpw(os.urandom(32), bcrypt.gensalt()).decode())
PY

Recap

  • Two expiries keep sessions short yet user-friendly.
  • A reactive filter parses the token and attaches authorities.
  • A database auth manager handles login, password checks, and audit logs.
  • Spring WebFlux security is glued together with one concise SecurityWebFilterChain.

References


Personal Update

Setup JioFiber at home, disconnected Airtel in Chennai, and ported all my SIMs to Jio except mom’s. Jio offers unlimited data (4G/5G) on individual plans for $8.77 per month. Nowadays I don’t have enough time to do anything. No no no…not gonna start a rant here…lol. Talked to a friend about old WiFi hacking days…(more in the next few posts).