Spring Security WebFlux - body with Authentication
Asked Answered
A

3

7

I want to implement simple Spring Security WebFlux application.
I want to use JSON message like

{
   'username': 'admin', 
   'password': 'adminPassword'
} 

in body (POST request to /signin) to sign in my app.

What did I do?

I created this configuration

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {

    @Autowired
    private ReactiveUserDetailsService userDetailsService;

    @Autowired
    private ObjectMapper mapper;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(11);
    }

    @Bean
    public ServerSecurityContextRepository securityContextRepository() {
        WebSessionServerSecurityContextRepository securityContextRepository =
                new WebSessionServerSecurityContextRepository();

        securityContextRepository.setSpringSecurityContextAttrName("securityContext");

        return securityContextRepository;
    }

    @Bean
    public ReactiveAuthenticationManager authenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);

        authenticationManager.setPasswordEncoder(passwordEncoder());

        return authenticationManager;
    }

    @Bean
    public AuthenticationWebFilter authenticationWebFilter() {
        AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());

        filter.setSecurityContextRepository(securityContextRepository());
        filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
        filter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
        );

        return filter;
    }



    @Bean
    public Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
        return exchange -> {
            return exchange.getRequest().getBody()
                    .cache()
                    .next()
                    .flatMap(body -> {
                        byte[] bodyBytes = new byte[body.capacity()];
                        body.read(bodyBytes);
                        String bodyString = new String(bodyBytes);
                        body.readPosition(0);
                        body.writePosition(0);
                        body.write(bodyBytes);

                        try {
                            UserController.SignInForm signInForm = mapper.readValue(bodyString, UserController.SignInForm.class);

                            return Mono.just(
                                    new UsernamePasswordAuthenticationToken(
                                            signInForm.getUsername(),
                                            signInForm.getPassword()
                                    )
                            );
                        } catch (IOException e) {
                            return Mono.error(new LangDopeException("Error while parsing credentials"));
                        }
                    });
        };
    }

    @Bean
    public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity,
                                                          ReactiveAuthenticationManager authenticationManager) {
        return httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .logout().disable()
                .formLogin().disable()
                .securityContextRepository(securityContextRepository())
                .authenticationManager(authenticationManager)
                .authorizeExchange()
                    .anyExchange().permitAll()
                .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .build();
    }

}

BUT I use jsonBodyAuthenticationConverter() and it reads Body of the incoming request. Body can be read only once, so I have an error

java.lang.IllegalStateException: Only one connection receive subscriber allowed.

Actually it's working but with exception (session is set in cookies). How can I remake it without this error?

Now I only created something like:

@PostMapping("/signin")
public Mono<Void> signIn(@RequestBody SignInForm signInForm, ServerWebExchange webExchange) {
    return Mono.just(signInForm)
               .flatMap(form -> {
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                            form.getUsername(),
                            form.getPassword()
                    );

                    return authenticationManager
                            .authenticate(token)
                            .doOnError(err -> {
                                System.out.println(err.getMessage());
                            })
                            .flatMap(authentication -> {
                                SecurityContextImpl securityContext = new SecurityContextImpl(authentication);

                                return securityContextRepository
                                        .save(webExchange, securityContext)
                                        .subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
                            });
                });
    }

And removed AuthenticationWebFilter from config.

Ahola answered 25/4, 2018 at 6:46 Comment(1)
I made a functional example project implementing webflux security + jwt, hope it helps github.com/eriknyk/webflux-jwt-security-demoAcceptation
D
6

You are almost there. The following converter worked for me:

public class LoginJsonAuthConverter implements Function<ServerWebExchange, Mono<Authentication>> {

    private final ObjectMapper mapper;

    @Override
    public Mono<Authentication> apply(ServerWebExchange exchange) {
        return exchange.getRequest().getBody()
                .next()
                .flatMap(buffer -> {
                    try {
                        SignInRequest request = mapper.readValue(buffer.asInputStream(), SignInRequest.class);
                        return Mono.just(request);
                    } catch (IOException e) {
                        log.debug("Can't read login request from JSON");
                        return Mono.error(e);
                    }
                })
                .map(request -> new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
    }
}

Furthermore, you don't need the sign in controller; spring-security will check each request for you in the filter. Here's how I configured spring-security with an ServerAuthenticationEntryPoint:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ReactiveAuthenticationManager authManager) {
    return http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers("/api/**").authenticated()
            .pathMatchers("/**", "/login", "/logout").permitAll()
            .and().exceptionHandling().authenticationEntryPoint(restAuthEntryPoint)
            .and().addFilterAt(authenticationWebFilter(authManager), SecurityWebFiltersOrder.AUTHENTICATION)
            .logout()
            .and().build();
}

Hope this helps.

Delisadelisle answered 25/4, 2018 at 14:12 Comment(0)
A
1

Finally I config WebFlux security so (pay attention to logout handling, logout doesn't have any standard ready-for-use configuration for 5.0.4.RELEASE, you must disable default logout config anyway, because default logout spec creates new SecurityContextRepository by default and doesn't allow you to set your repository).

UPDATE: default logout configuration doesn't work only in case when you set custom SpringSecurityContextAttributeName in SecurityContextRepository for web session.

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {

    @Autowired
    private ReactiveUserDetailsService userDetailsService;

    @Autowired
    private ObjectMapper mapper;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(11);
    }

    @Bean
    public ServerSecurityContextRepository securityContextRepository() {
        WebSessionServerSecurityContextRepository securityContextRepository =
                new WebSessionServerSecurityContextRepository();

        securityContextRepository.setSpringSecurityContextAttrName("langdope-security-context");

        return securityContextRepository;
    }

    @Bean
    public ReactiveAuthenticationManager authenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);

        authenticationManager.setPasswordEncoder(passwordEncoder());

        return authenticationManager;
    }

    @Bean
    public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity) {
        return httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                .securityContextRepository(securityContextRepository())
                .authorizeExchange()
                    .anyExchange().permitAll() // Currently
                .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .addFilterAt(logoutWebFilter(), SecurityWebFiltersOrder.LOGOUT)
                .build();
    }

    private AuthenticationWebFilter authenticationWebFilter() {
        AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());

        filter.setSecurityContextRepository(securityContextRepository());
        filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
        filter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home"));
        filter.setAuthenticationFailureHandler(
                new ServerAuthenticationEntryPointFailureHandler(
                        new RedirectServerAuthenticationEntryPoint("/authentication-failure")
                )
        );
        filter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
        );

        return filter;
    }

    private LogoutWebFilter logoutWebFilter() {
        LogoutWebFilter logoutWebFilter = new LogoutWebFilter();

        SecurityContextServerLogoutHandler logoutHandler = new SecurityContextServerLogoutHandler();
        logoutHandler.setSecurityContextRepository(securityContextRepository());

        RedirectServerLogoutSuccessHandler logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
        logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));

        logoutWebFilter.setLogoutHandler(logoutHandler);
        logoutWebFilter.setLogoutSuccessHandler(logoutSuccessHandler);
        logoutWebFilter.setRequiresLogoutMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")
        );

        return logoutWebFilter;
    }

    private Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
        return exchange -> exchange
                .getRequest()
                .getBody()
                .next()
                .flatMap(body -> {
                    try {
                        UserController.SignInForm signInForm =
                                mapper.readValue(body.asInputStream(), UserController.SignInForm.class);

                        return Mono.just(
                                new UsernamePasswordAuthenticationToken(
                                        signInForm.getUsername(),
                                        signInForm.getPassword()
                                )
                        );
                    } catch (IOException e) {
                        return Mono.error(new LangDopeException("Error while parsing credentials"));
                    }
                });
    }

}
Ahola answered 26/4, 2018 at 20:35 Comment(0)
B
0

You can just save the security context in the repository and optionally put it in the context:

@Autowired
private ServerSecurityContextRepository securityContextRepository;

@PostMapping("/signin")
public Mono<Void> signIn(@RequestBody SignInForm signInForm, ServerWebExchange webExchange) {
    // TODO check credentials
    SecurityContext securityContext = new SecurityContextImpl(new UsernamePasswordAuthenticationToken(user, null))
    return securityContextRepository
        .save(webExchange, securityContext)
        .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
}

The line with contextWrite is only required in case to use the SecurityContext in the ongoing request. In subsequent requests, the AuthorizationFilter will establish the context anyway.

Breathtaking answered 22/8, 2023 at 11:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.