RSocket Authentication/Authorization using Spring Security 5.2.0.RC1

Author: Shazin Sadakath


We spoke about RSocket in Spring Boot 2.2 in a post recently. Now with Spring Security 5.2.0 Release Candidate 1 (RC1) support for RSocket has been released. So we wanted to talk about how to make use of it to protect inter RSocket communication using Spring Security. For this purpose we are using a RSocket Producer and Consumer similar to our previous post. But this time Producer message endpoints will be protected using username/password combination and Consumer will Authenticate using correct credentials to access resources.

Lets have a look at the Producer side first.

In Producer side we mainly have a Security Configuration class which uses @EnableRSocketSecurity annotation and configures a PayloadSocketAcceptorInterceptor which will be used by the RSocketServer to do protect the message endpoints. Mainly there are two types of protection Setup (Ability to connect to an RSocketServer) and protection of invoking Endpoints.

@Configuration
@EnableRSocketSecurity
public class SecurityConfig {

    @Bean
    public RSocketMessageHandler messageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
                .decoder(new BasicAuthenticationDecoder(), new Jackson2JsonDecoder())
                .encoder(new Jackson2JsonEncoder())
                .build();
    }

    @Bean
    public PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
        rsocket.authorizePayload(authorize -> {
            authorize
                    // must have ROLE_SETUP to make connection
                    .setup().hasRole("SETUP")
                    // must have ROLE_ADMIN for routes starting with "taxis."
                    .route("taxis*").hasRole("ADMIN")
                    // any other request must be authenticated for
                    .anyRequest().authenticated();
        })
        .basicAuthentication(Customizer.withDefaults());

        return rsocket.build();
    }

    @Bean
    public MapReactiveUserDetailsService userDetailsService() {
        UserDetails adminUser = User.withDefaultPasswordEncoder().username("shazin").password("sha123").roles("ADMIN").build();

        UserDetails setupUser = User.withDefaultPasswordEncoder().username("setup").password("sha123").roles("SETUP").build();

        return new MapReactiveUserDetailsService(adminUser, setupUser);
    }

}

We are using Basic Authentication thus we need to BasicAuthenticationDecoder in RSocketStrategies. Finally we need to make use of our PayloadSocketAcceptorInterceptor in our RSocketServer for that we need the following beans setup.

    @Bean
 ReactorResourceFactory reactorResourceFactory() {
  return new ReactorResourceFactory();
 }

 @Bean
 RSocketServerFactory rSocketServerFactory(ReactorResourceFactory resourceFactory,
             ObjectProvider customizers) throws Exception {
  NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
  factory.setResourceFactory(resourceFactory);
  factory.setTransport(RSocketServer.TRANSPORT.TCP);
  PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
  map.from(InetAddress.getByName("localhost")).to(factory::setAddress);
  map.from(7000).to(factory::setPort);
  factory.setServerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
  return factory;
 }

 @Bean
 RSocketServerBootstrap rSocketServerBootstrap(RSocketServerFactory rSocketServerFactory,
              RSocketMessageHandler rSocketMessageHandler) {
  return new RSocketServerBootstrap(rSocketServerFactory, rSocketMessageHandler.responder());
 }

 @Bean
 ServerRSocketFactoryCustomizer frameDecoderServerFactoryCustomizer(
   RSocketMessageHandler rSocketMessageHandler, PayloadSocketAcceptorInterceptor rsocketInterceptor) {
  return (serverRSocketFactory) -> {
   if (rSocketMessageHandler.getRSocketStrategies()
     .dataBufferFactory() instanceof NettyDataBufferFactory) {
    serverRSocketFactory.frameDecoder(PayloadDecoder.ZERO_COPY);
   }
   return serverRSocketFactory.addSocketAcceptorPlugin(rsocketInterceptor);
  };
 }

We use the frameDecoderServerFactoryCustomizer bean to add the PayloadSocketAcceptorInterceptor as a Socket Acceptor Plugin. This should do the trick for the Producer end protection. 

Now we should have a look at the Consumer side Authentication.

In the Consumer side we need to have following beans defined.

    @Bean
 public RSocketRequester rSocketRequester(RSocketStrategies rSocketStrategies) {
  UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("setup", "sha123");
  return RSocketRequester.builder()
    .dataMimeType(MimeTypeUtils.APPLICATION_JSON)
    .rsocketStrategies(rSocketStrategies)
    .rsocketFactory(clientRSocketFactory -> {
     clientRSocketFactory.frameDecoder(PayloadDecoder.ZERO_COPY);
    })
    .setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
    .connect(TcpClientTransport.create(TcpClient.create().port(7000)))
    .block();
 }

 @Bean
 public RSocketStrategies rsocketStrategies() {
  return RSocketStrategies.builder()
    .encoder(new BasicAuthenticationEncoder(), new Jackson2JsonEncoder())
    .decoder(new Jackson2JsonDecoder())
    .build();
 }

In the RSocketStrategies bean we need to have BasicAuthenticationEncoder to send the Basic Authentication credentials to Producer. And in RSocketRequester bean during setup we use credentials for "setup" user who has role ROLE_SETUP. And during each invocation of the message endpoints we again using Basic Authentication this time with a credentials for "shazin" user who has role ROLE_ADMIN and can access the protected endpoints as below.

@RestController
class TaxisRestController {

 private final RSocketRequester rSocketRequester;

 TaxisRestController(RSocketRequester rSocketRequester) {
  this.rSocketRequester = rSocketRequester;
 }

 @GetMapping("/taxis/{type}/{from}/{to}")
 public Publisher taxis(@PathVariable String type, @PathVariable String from, @PathVariable String to) {
  UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("shazin", "sha123");
  return rSocketRequester
    .route("taxis")
    .metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
    .data(new TaxisRequest(type, from, to))
    .retrieveMono(TaxisResponse.class);
 }

 @GetMapping(value = "/taxis/sse/{type}/{from}/{to}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
 public Publisher taxisStream(@PathVariable String type, @PathVariable String from, @PathVariable String to) {
  UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("shazin", "sha123");
  return rSocketRequester
    .route("taxis-stream")
    .metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
    .data(new TaxisRequest(type, from, to))
    .retrieveFlux(TaxisResponse.class);
 }

}

The Basic Authentication credentials are passed as Metadata of the request so we need to send those in metadata accordingly. The complete source code can be found in https://github.com/shazin/rsocket-spring-security-demo



Tags: RSocket SpringBoot 2 Reactive Socket SpringSecurity Basic
Views: 500
Register for more exciting articles

Comments

Please login or register to post a comment.


There are currently no comments.