2

I have set up keycloak with a google identity provider. And I have set up a simple reactive spring-boot web application with spring security and MongoDB. I want to save users after they successfully pass the authorization filter. Here is my security configuration:

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Slf4j
@RequiredArgsConstructor
public class SecurityConfiguration {

    private final UserSavingFilter userSavingFilter;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange()
                .anyExchange().authenticated()
                .and()
                .addFilterAfter(userSavingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .oauth2ResourceServer()
                .jwt();

        return http.build();
    }

}

And here is my filter for saving users:

public class UserSavingFilter implements WebFilter {

    private final ObjectMapper objectMapper;

    private final UserService userService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        Base64.Decoder decoder = Base64.getUrlDecoder();
        var authHeader = getAuthHeader(exchange);
        if (authHeader == null) {
            return chain.filter(exchange);
        }
        var encodedPayload = authHeader.split("Bearer ")[1].split("\\.")[1];
        var userDetails = convertToMap(new String(decoder.decode(encodedPayload)));
        saveUserIfNotPresent(userDetails);

        return chain.filter(exchange);
    }

    @SneakyThrows
    private void saveUserIfNotPresent(Map<String, Object> map) {
        var userEmail = String.valueOf(map.get("email"));

        var userPresent = userService.existsByEmail(userEmail).toFuture().get();

        if (userPresent) {
            return;
        }

        log.info("Saving new user with email: {}", userEmail);

        var user = new User();
        user.setEmail(userEmail);
        user.setFirstName(String.valueOf(map.get("given_name")));
        user.setLastName(String.valueOf(map.get("family_name")));
        userService.save(user).subscribe(User::getId);
    }

    @SuppressWarnings("java:S2259")
    private String getAuthHeader(ServerWebExchange exchange) {
        var authHeaders = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION);
        if (authHeaders == null) {
            return null;
        }
        return authHeaders.get(0);
    }

    @SneakyThrows
    private Map<String, Object> convertToMap(String payloadJson) {
        return objectMapper.readValue(payloadJson,Map.class);
    }
}

Problems:

  1. For some reason, my filter executes twice per request. I can see 2 log messages about saving new user.
  2. When I call getAll() endpoint, it does not return the user saved in this request in the filter.

Probably it is not the best way to save users, but I could not find an alternative to successHandler for the resource server with jwt. Please suggest how can I solve those two problems.

eternal
  • 339
  • 2
  • 15
  • 2
    By any chance is your filter annotated with `@Component` ? This could explain why it is called twice, as Spring Boot automatically registers any bean that is a Filter with the servlet container (see [documentation](https://docs.spring.io/spring-boot/docs/1.5.4.RELEASE/reference/html/howto-embedded-servlet-containers.html)). Then you said `getAll()` does not return the user just saved, but are you sure the user is actually saved ? – Yann39 Aug 24 '22 at 18:23
  • 1
    For the double save, isn't it a race-condition between OPTION and actual request (GEST / POST / PUT / DELETE)? Side note: why don't you use the `JwtAuthenticationToken` in the security-context which contains the JWT claims? – ch4mp Aug 24 '22 at 18:29
  • @Yann39 you are totally right. My filter was annotated with `@Component` and that is why it was executed twice. It is interesting logic from spring. If I make a filter and annotate it with `@Component`, at which place in the filter chain spring will place my filter? I am just curious since I could not find it in the documentation. Probably just missed it. About getAll(). I believe it is because this is a reactive app. And saving user means that it will be saved in some time, but we can't be sure, that it will happen immediately, so that is why it doesn't return in response. – eternal Aug 24 '22 at 19:11
  • 2
    @ch4mp we probably resolved an issue with double save, thanks to you guys! About your note: great point. I just tried it but didn't succeed. `SecurityContextHolder.getContext()` returns `null` in my filter for some reason. I have also tried `ReactiveSecurityContextHolder` but the result is the same. I've managed to save my user, by calling `toFuture().get()` on my save method. Not the best idea probably, but it is what it is. @Yann39 you can submit an answer. I'll accept it. Thanks, guys! – eternal Aug 24 '22 at 19:18
  • other note, you could put your logic in a `ReactiveAuthenticationManager` instead of a filter: `http.oauth2ResourceServer().jwt().authenticationManager(authenticationManager())`. This would also allow you to populate security-context with an `Authentication` of your own, based on your User implementation, for instance. – ch4mp Aug 24 '22 at 21:59

1 Answers1

2

By any chance is your filter annotated with @Component ? This could explain why it is called twice, as Spring Boot automatically registers any bean that is a Filter with the servlet container (see documentation).

So you can setup a registration bean to disable it :

@Bean
public FilterRegistrationBean<UserSavingFilter> disableUserSavingFilter(final UserSavingFilter filter) {
    final FilterRegistrationBean<UserSavingFilter> filterRegistrationBean = new FilterRegistrationBean<>();
    filterRegistrationBean.setFilter(filter);
    filterRegistrationBean.setEnabled(false);
    return filterRegistrationBean;
}

By default, custom filter beans without information of ordering are automatically added to the main servlet filter chain at the very last position (actually with the lowest precedence, as if you would apply default order annotation @Order(Ordered.LOWEST_PRECEDENCE), see Order).

In debug level you should see in the logs the position of the filters when they are added to the chain, something like :

... at position 4 of 4 in additional filter chain; firing Filter: UserSavingFilter

About your second problem, if you are sure the user is actually saved (i.e. you find it into the database afterwards) then indeed it may just be because your getAll() method gets executed before your future call is completed.

Yann39
  • 14,285
  • 11
  • 56
  • 84