RSocket Security With Spring

Overview:

In this tutorial, I would like to demo RSocket Security with Spring.

If you are new to RSocket, take a look at these articles on RSocket.

RSocket Security:

RSocket is a great choice for client/server application development. It offers 4 different interaction models for client-server communication which we had seen as part of previous articles here. But we have not discussed these!

  • How to secure our application so that only any trusted client can access the application?
  • How to provide role based access

rsocket security

Lets discuss those in this article.

Project Setup:

Create a Spring Boot application with below dependencies.

 

RSocket Security Configuration:

Lets create a configuration class for RSocket security in which we add a configuration that only a trusted client can access the application.

@Configuration
@EnableRSocketSecurity
@EnableReactiveMethodSecurity
public class RSocketSecurityConfig {

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

    @Bean
    public RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.getArgumentResolverConfigurer().addCustomResolver(new AuthenticationPrincipalArgumentResolver());
        handler.setRSocketStrategies(strategies);
        return handler;
    }

    @Bean
    public PayloadSocketAcceptorInterceptor authorization(RSocketSecurity security) {
        security.authorizePayload(authorize ->
                authorize
                       .setup().hasRole("TRUSTED_CLIENT")
                       .anyRequest()
                       .authenticated()
        ).simpleAuthentication(Customizer.withDefaults());
        return security.build();
    }

}

User Repository:

I create a Map as a Database just for this demo.

@Configuration
public class UserDB {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public Map<String, UserDetails> map(){
        return Map.of(
                "user", User.withUsername("user").password(passwordEncoder.encode("user")).roles("USER").build(),
                "admin", User.withUsername("admin").password(passwordEncoder.encode("admin")).roles("ADMIN").build(),
                "client", User.withUsername("client").password(passwordEncoder.encode("client")).roles("TRUSTED_CLIENT").build()
        );
    }

}

Reactive User Details Service:

We need an implementation to provide the user details based on the username.

@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {

    @Autowired
    private Map<String, UserDetails> map;

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        return Mono.just(this.map.get(username));
    }

}

Secured Controller:

This is our controller with endpoints specific to roles.

@Controller
public class SecuredController {

    @PreAuthorize("hasRole('USER')")
    @MessageMapping("user-request")
    public Mono<Integer> findDouble(final int requestNo) {
        return Mono.just(requestNo * 2);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @MessageMapping("admin-request")
    public Mono<Integer> findTriple(final int requestNo) {
        return Mono.just(requestNo * 3);
    }

}

Credentials:

I just create a class for keeping credentials for testing purposes.

public class Credentials {

    public static final UsernamePasswordMetadata WRONG_SETUP_CREDENTIALS = new UsernamePasswordMetadata("client", "wrong");
    public static final UsernamePasswordMetadata CORRECT_SETUP_CREDENTIALS = new UsernamePasswordMetadata("client", "client");
    public static final UsernamePasswordMetadata WRONG_USER_CREDENTIALS = new UsernamePasswordMetadata("user", "wrong");
    public static final UsernamePasswordMetadata CORRECT_USER_CREDENTIALS = new UsernamePasswordMetadata("user", "user");
    public static final UsernamePasswordMetadata WRONG_ADMIN_CREDENTIALS = new UsernamePasswordMetadata("admin", "wrong");
    public static final UsernamePasswordMetadata CORRECT_ADMIN_CREDENTIALS = new UsernamePasswordMetadata("admin", "admin");

}

Demo:

  • Case 1 – wrong client setup credentials which will throw Rejected Setup exception
@SpringBootTest
class RSocketSecurityApplicationTests {

    private static final MimeType MIME_TYPE = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());

    @Autowired
    private RSocketRequester.Builder builder;

    @Test
    void wrongSetupCredentials()  {
        RSocketRequester rSocketRequester = builder
                .setupMetadata(Credentials.WRONG_SETUP_CREDENTIALS, MIME_TYPE)
                .tcp("localhost", 6565);

        Mono<Integer> mono = rSocketRequester.route("user-request")
                .metadata(Credentials.CORRECT_USER_CREDENTIALS, MIME_TYPE)
                .data(5)
                .retrieveMono(Integer.class);

        StepVerifier.create(mono)
                .verifyError(RejectedSetupException.class);
    }
    
}
  • Case 2 – Correct setup – but wrong user credentials for the request – This will throw access denied error
RSocketRequester rSocketRequester = builder
        .setupMetadata(Credentials.CORRECT_SETUP_CREDENTIALS, MIME_TYPE)
        .tcp("localhost", 6565);


// wrong user credentials
Mono<Integer> mono = rSocketRequester.route("user-request")
        .metadata(Credentials.WRONG_USER_CREDENTIALS, MIME_TYPE)
        .data(5)
        .retrieveMono(Integer.class);

StepVerifier.create(mono)
        .verifyError(ApplicationErrorException.class);
  • Case 3 – Correct setup – correct user credentials – We should expect correct result.
RSocketRequester rSocketRequester = builder
        .setupMetadata(Credentials.CORRECT_SETUP_CREDENTIALS, MIME_TYPE)
        .tcp("localhost", 6565);

// correct user credentials
Mono<Integer> mono = rSocketRequester.route("user-request")
        .metadata(Credentials.CORRECT_USER_CREDENTIALS, MIME_TYPE)
        .data(5)
        .retrieveMono(Integer.class);

StepVerifier.create(mono)
        .expectNext(10)
        .verifyComplete();
  • Case 4 – Correct credentials – Insufficient privileges.
RSocketRequester rSocketRequester = builder
        .setupMetadata(Credentials.CORRECT_SETUP_CREDENTIALS, MIME_TYPE)
        .tcp("localhost", 6565);

// correct user credentials
// but cannot access admin endpoints
Mono<Integer> mono = rSocketRequester.route("admin-request")
        .metadata(Credentials.CORRECT_USER_CREDENTIALS, MIME_TYPE)
        .data(5)
        .retrieveMono(Integer.class);

StepVerifier.create(mono)
        .verifyError(ApplicationErrorException.class);

Summary:

We were able to successfully demonstrate RSocket Security with Spring & we get appropriate results based on the credentials/privileges.

Read more about RSocket.

The source code is available here.

Happy learning 🙂

Share This:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.