0

I have a springboot webflux project with reactive security enabled For some reason the project seems to be calling the authenticate method of ReactiveAuthenticationManager twice (once in the default handler and once the request reaches the controller).

Here is my sample code WebSecurityConfig.class

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebSecurityConfig {

  static final String[] AUTH_WHITELIST = {
    "/swagger-resources/**",
    "/swagger-ui.html",
    "/v2/api-docs",
    "/v3/api-docs",
    "/webjars/**",
    "/swagger-ui/**",
    "/v1/healthcheck"
  };
  private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
  private final AuthenticationManager authenticationManager;
  private final SecurityContextRepository securityContextRepository;

  public WebSecurityConfig(
      JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
      AuthenticationManager authenticationManager,
      SecurityContextRepository securityContextRepository) {
    this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
    this.authenticationManager = authenticationManager;
    this.securityContextRepository = securityContextRepository;
  }

  @Bean
  public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http.securityMatcher(
            new NegatedServerWebExchangeMatcher(
                ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST)))
        .exceptionHandling()
        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
        .accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.BAD_REQUEST))
        .and()
        .csrf()
        .disable()
        .formLogin()
        .disable()
        .httpBasic()
        .disable()
        .logout()
        .disable()
        .authenticationManager(authenticationManager)
        .securityContextRepository(securityContextRepository)
        .authorizeExchange()
        .pathMatchers(HttpMethod.OPTIONS)
        .permitAll()
        .anyExchange()
        .authenticated()
        .and()
        .build();
  }

ServerSecurityContextRepository.class

@Slf4j
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {

  private final AuthenticationService authenticationService;
  private final AuthenticationManager authenticationManager;

  public SecurityContextRepository(
      AuthenticationService authenticationService, AuthenticationManager authenticationManager) {
    this.authenticationService = authenticationService;
    this.authenticationManager = authenticationManager;
  }

  @Override
  public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
    throw new UnsupportedOperationException("Not supported yet.");
  }

  @Override
  public Mono<SecurityContext> load(ServerWebExchange swe) {
    ServerHttpRequest request = swe.getRequest();
    log.info("Parsing Authorization token from Request");
    AuthToken authToken =
        authenticationService.parseRequestToken(authenticationService.getHeaders(request));
    Authentication auth = new UsernamePasswordAuthenticationToken(authToken, null);
    return this.authenticationManager
        .authenticate(auth)
        .map(authentication -> (SecurityContext) new SecurityContextImpl(authentication))

ReactiveAuthenticationManager.class

@Slf4j
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {

  final AuthenticationService authenticationService;

  @Value("${app.auth_enable}")
  private boolean isAuthEnabled;

  public AuthenticationManager(AuthenticationService authenticationService) {
    this.authenticationService = authenticationService;
  }

  @Override
  public Mono<Authentication> authenticate(Authentication authentication) {
    AuthToken token = (AuthToken) authentication.getPrincipal();
    if (Objects.isNull(token)) {
      log.error("Jwt token not provided");
      return Mono.error(new AuthorizeException("Jwt token not provided"));
    }
    if (isAuthEnabled) {
      return authenticationService
          .verifyRequestToken(token)
          .map(
              aBoolean -> {
                if (!aBoolean) {
                  log.warn("Jwt token not valid");
                  return null;
                }
                log.info("Jwt token is valid");
                return new UsernamePasswordAuthenticationToken(token, null, null);
              });
    }
    return Mono.just(new UsernamePasswordAuthenticationToken(token, null, null));
  }

JwtAuthenticationEntryPoint.class

@Slf4j
@Component
public class JwtAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint
    implements Serializable {

  private final ObjectMapper objectMapper;

  public JwtAuthenticationEntryPoint(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
  }

  @Override
  public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
    log.info("Commencing AuthenticationEntryPoint...");
    ServerHttpResponse response = exchange.getResponse();
    JwtAuthenticationError error =
        new JwtAuthenticationError(JwtExceptionContext.getExceptionContext());
    JwtExceptionContext.clearExceptionContext();
    byte[] bytes = new byte[0];
    try {
      bytes = objectMapper.writeValueAsString(error).getBytes(StandardCharsets.UTF_8);
    } catch (JsonProcessingException e) {
      log.error("JsonProcessingException on commence function : {}", e.getMessage(), e);
    }
    DataBuffer buffer = response.bufferFactory().wrap(bytes);
    response.setStatusCode(HttpStatus.valueOf(Integer.parseInt(error.getStatusCode())));
    log.warn(
        "Authentication Failed: {} -> {}",
        value("errorMsg", error),
        keyValue(
            AppConstants.STATUS_CODE, HttpStatus.valueOf(Integer.parseInt(error.getStatusCode()))));
    return response.writeWith(Mono.just(buffer));
  }

Sample logs

2022-01-18 15:30:25.203 DEBUG 9308 --- [cTaskExecutor-1] o.s.web.client.RestTemplate              : Reading to [java.lang.String] as "application/json"
2022-01-18 15:30:25.209  INFO 9308 --- [cTaskExecutor-1] c.s.p.r.n.h.s.AuthenticationServiceImpl  : Validation Response 200
2022-01-18 15:30:25.209  INFO 9308 --- [oundedElastic-1] c.s.p.r.n.h.s.AuthenticationManager      : **Jwt token is valid**
2022-01-18 15:30:25.211 DEBUG 9308 --- [oundedElastic-1] o.s.s.w.s.a.AuthorizationWebFilter       : Authorization successful
2022-01-18 15:30:25.217 DEBUG 9308 --- [oundedElastic-1] s.w.r.r.m.a.RequestMappingHandlerMapping : [1fc32c7d-1, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:54225] Mapped to MyController#getCount(Boolean, String, UsernamePasswordAuthenticationToken)
2022-01-18 15:30:25.255  INFO 9308 --- [oundedElastic-1] c.s.p.r.n.h.s.AuthenticationServiceImpl  : Validation Response 200
2022-01-18 15:30:25.256  INFO 9308 --- [oundedElastic-2] c.s.p.r.n.h.s.AuthenticationManager      : **Jwt token is valid**

Any suggestions or pointers are appreciated. Thanks in advance

  • My first question, why the unorthodox custom implementation, and why not customize the standard jwt implementation that comes with spring security which means you can probably throw away about 75% of your code. Please read the entire chapter in the spring security reference documentation on jwts that shows how to implement the handling of jwts in spring security – Toerktumlare Jan 18 '22 at 13:29
  • Also your problem probably stems from that all you custom code are spring beans that are automatically registered into the context, but for some strange reason you are also manually setting them in your security configuration. So they are registered twice – Toerktumlare Jan 18 '22 at 13:36
  • @Toerktumlare thanks for responding. The custom implementation is required for us as the JWT token default validates iss from token, which is not there in our token and the endpoints to validate the token has other headers and params due to which we found this to be easier. – Ashmeet Kandhari Jan 20 '22 at 16:00
  • You are aware that you can customize all that using springs jwtfilter? Also, not including iss is a security risk, and diverting from the rfc because it is ”easier” is also a security risk. Rfcs are defined to provide a standard, divirting from a standard is bad practice and should be avoided. – Toerktumlare Jan 20 '22 at 18:26
  • https://curity.io/resources/learn/jwt-best-practices/#4-always-check-the-issuer – Toerktumlare Jan 20 '22 at 18:31

0 Answers0