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
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 🙂