I have two SpringBoot apps, a SpringBoot RSocket Client app and a SpringBoot RSocket Server app. I'm trying to add security with OAuth2 and OIDC by using Keycloak. From what I've been reading it seems like the Client Credentials flow is the best for service-to-service communication, so that's what I've been working on.
As of now I managed to make this work through the following code on my RSocket client app:
@EnableWebFluxSecurity
public class WebFluxSecurityConfiguration {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/api/v1/**").permitAll()
.and().oauth2Client()
.and().oauth2ResourceServer().jwt()
.and().and()
.build();
}
}
@Component
@RequiredArgsConstructor
public class OAuth2 {
private final ReactiveClientRegistrationRepository clientRepository;
@Value("${oauth2.client.registration-id}")
private String clientRegistrationId;
public Mono<String> getAccessToken() {
var client = new WebClientReactiveClientCredentialsTokenResponseClient();
return clientRepository
.findByRegistrationId(clientRegistrationId)
.map(OAuth2ClientCredentialsGrantRequest::new)
.flatMap(client::getTokenResponse)
.map(OAuth2AccessTokenResponse::getAccessToken)
.map(OAuth2AccessToken::getTokenValue);
}
public Consumer<RSocketRequester.MetadataSpec<?>> addTokenToMetadata(String token) {
var mimeType = MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString());
var bearerTokenMetadata = new BearerTokenMetadata(token);
return spec -> spec.metadata(bearerTokenMetadata, mimeType);
}
}
@Component
public class FooClientImpl implements FooClient {
private final Mono<RSocketRequester> rsocketRequester;
private final OAuth2 oAuth2;
public FooClientImpl(RSocketRequester.Builder requesterBuilder,
OAuth2 oAuth2,
@Value("${routes.foo-service.host}") String host,
@Value("${routes.foo-service.port}") Integer port) {
this.rsocketRequester = requesterBuilder
.rsocketStrategies(configurer -> configurer.encoder(new BearerTokenAuthenticationEncoder()))
.dataMimeType(MediaType.APPLICATION_CBOR)
.connectTcp(host, port);
this.oAuth2 = oAuth2;
}
@Override
public Publisher<Foo> findFoos(FooRequest request) {
return oAuth2.getAccessToken()
.flatMapMany(accessToken -> rsocketRequester
.flatMapMany(req -> req
.route("find.foos")
.metadata(oAuth2.addTokenToMetadata(accessToken))
.data(request)
.retrieveFlux(Foo.class)));
}
}
My application.yml configuration file:
server:
port: 9000
logging:
level:
root: info
---
spring:
profiles: local
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:8080/auth/realms/foo
registration:
keycloak:
client-id: foo-service
client-secret: 81196896-80bd-41e3-97e6-3556feeef577
authorization-grant-type: client_credentials
resourceserver:
jwt:
issuer-uri: http://localhost:8080/auth/realms/foo
routes:
foo-service:
host: localhost
port: 7000
oauth2:
client:
registration-id: keycloak
Although if I, for instance, was using HTTP instead of RSocket, I'd be able to leverage Spring's WebClient like this instead:
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
oauth.setDefaultClientRegistrationId("keycloak");
return WebClient
.builder()
.filter(oauth)
.build();
}
Is there any better way to achieve this? Is there a way to call the RSocket Server (through RSocketRequester) with an access token through a filter function (or something similar) like I would do with the WebClient? Is there any support for OAuth2 Clients in Spring Security RSocket? I've looked up the Spring Rsocket documentation but they only mention general jwt support and I haven't found anything about OAuth2 support.