Search code examples
javaspring-bootspring-securityproject-reactoroauth2resourceserver

Save user after success authentication


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.


Solution

  • 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.