Any better way to implement OAuth2 client credentials with Spring RSocket?

Viewed 622

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.

1 Answers

Although its been a while but sharing my experience with cli

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;

@Configuration
@EnableReactiveMethodSecurity
@EnableRSocketSecurity
public class RSocketSecurityConfig {

    @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) {
        return security
                .authorizePayload(authorizeSpec -> authorizeSpec
                        .route(rsocket_route).authenticated()
                        .anyExchange().permitAll())
                .jwt(Customizer.withDefaults())
                .build();
    }

    @Bean
    public ReactiveJwtDecoder jwtDecoder() {
        return ReactiveJwtDecoders
                .fromIssuerLocation("http://KEYCLOAK_SERVER/auth/realms/YOUR_REALM");
    }
}

The dependencies I've used

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-messaging</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-rsocket</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

Testing using RSC

rsc tcp://localhost:7000 --authBearer 'THE_JWT_TOKEN' -r rsocket_route
Related