From 7414684714f548ec417bc9a2f5f2a86abd37a9d1 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Tue, 5 Aug 2025 00:12:02 +0300 Subject: [PATCH] Add Disabling Anonymous Authentication in RSocketSecurity Closes: gh-17132 Signed-off-by: Andrey Litvitski 1 Signed-off-by: Andrey Litvitski 1 Signed-off-by: Andrey Litvitski --- .../AnonymousAuthenticationITests.java | 146 ++++++++++++++++++ .../rsocket/RSocketMessageHandlerITests.java | 3 +- .../annotation/rsocket/RSocketSecurity.java | 39 ++++- 3 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/AnonymousAuthenticationITests.java diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/AnonymousAuthenticationITests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/AnonymousAuthenticationITests.java new file mode 100644 index 0000000000..9ac1952c71 --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/AnonymousAuthenticationITests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.rsocket; + +import java.util.ArrayList; +import java.util.List; + +import io.rsocket.core.RSocketServer; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; +import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; +import org.springframework.security.rsocket.util.matcher.PayloadExchangeAuthorizationContext; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Andrey Litvitski + */ +@ContextConfiguration +@ExtendWith(SpringExtension.class) +public class AnonymousAuthenticationITests { + + @Autowired + RSocketMessageHandler handler; + + @Autowired + SecuritySocketAcceptorInterceptor interceptor; + + @Autowired + ServerController controller; + + private CloseableChannel server; + + private RSocketRequester requester; + + @BeforeEach + public void setup() { + // @formatter:off + this.server = RSocketServer.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .interceptors((registry) -> registry.forSocketAcceptor(this.interceptor) + ) + .acceptor(this.handler.responder()) + .bind(TcpServerTransport.create("localhost", 0)) + .block(); + // @formatter:on + } + + @AfterEach + public void dispose() { + this.requester.rsocket().dispose(); + this.server.dispose(); + this.controller.payloads.clear(); + } + + @Test + public void requestWhenAnonymousDisabledThenRespondsWithForbidden() { + this.requester = RSocketRequester.builder() + .rsocketStrategies(this.handler.getRSocketStrategies()) + .connectTcp("localhost", this.server.address().getPort()) + .block(); + String data = "andrew"; + assertThatExceptionOfType(RejectedSetupException.class).isThrownBy( + () -> this.requester.route("secure.retrieve-mono").data(data).retrieveMono(String.class).block()); + assertThat(this.controller.payloads).isEmpty(); + } + + @Configuration + @EnableRSocketSecurity + static class Config { + + @Bean + ServerController controller() { + return new ServerController(); + } + + @Bean + RSocketMessageHandler messageHandler() { + return new RSocketMessageHandler(); + } + + @Bean + PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { + AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + ReactiveAuthorizationManager anonymous = (authentication, + exchange) -> authentication.map(trustResolver::isAnonymous).map(AuthorizationDecision::new); + rsocket.authorizePayload((authorize) -> authorize.anyExchange().access(anonymous)); + return rsocket.build(); + } + + } + + @Controller + static class ServerController { + + private List payloads = new ArrayList<>(); + + @MessageMapping("**") + String retrieveMono(String payload) { + add(payload); + return "Hi " + payload; + } + + private void add(String p) { + this.payloads.add(p); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java index ad1a993e63..af701ed7ec 100644 --- a/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java @@ -268,7 +268,8 @@ PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { .route("secure.*").authenticated() .anyExchange().permitAll() ) - .basicAuthentication(Customizer.withDefaults()); + .basicAuthentication(Customizer.withDefaults()) + .anonymousAuthentication(Customizer.withDefaults()); // @formatter:on return rsocket.build(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java index 36a3c9acbd..03320b83a0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/rsocket/RSocketSecurity.java @@ -109,6 +109,7 @@ * @author Manuel Tejeda * @author Ebert Toribio * @author Ngoc Nhan + * @author Andrey Litvitski * @since 5.2 */ public class RSocketSecurity { @@ -119,6 +120,8 @@ public class RSocketSecurity { private SimpleAuthenticationSpec simpleAuthSpec; + private AnonymousAuthenticationSpec anonymousAuthSpec; + private JwtSpec jwtSpec; private AuthorizePayloadsSpec authorizePayload; @@ -164,6 +167,19 @@ public RSocketSecurity simpleAuthentication(Customizer return this; } + /** + * Adds anonymous authentication + * @param anonymous a customizer + * @return this instance + */ + public RSocketSecurity anonymousAuthentication(Customizer anonymous) { + if (this.anonymousAuthSpec == null) { + this.anonymousAuthSpec = new AnonymousAuthenticationSpec(); + } + anonymous.customize(this.anonymousAuthSpec); + return this; + } + /** * Adds authentication with BasicAuthenticationPayloadExchangeConverter. * @param basic @@ -214,7 +230,9 @@ private List payloadInterceptors() { if (this.jwtSpec != null) { result.addAll(this.jwtSpec.build()); } - result.add(anonymous()); + if (this.anonymousAuthSpec != null) { + result.add(this.anonymousAuthSpec.build()); + } if (this.authorizePayload != null) { result.add(this.authorizePayload.build()); } @@ -222,12 +240,6 @@ private List payloadInterceptors() { return result; } - private AnonymousPayloadInterceptor anonymous() { - AnonymousPayloadInterceptor result = new AnonymousPayloadInterceptor("anonymousUser"); - result.setOrder(PayloadInterceptorOrder.ANONYMOUS.getOrder()); - return result; - } - private T getBean(Class beanClass) { if (this.context == null) { return null; @@ -283,6 +295,19 @@ protected AuthenticationPayloadInterceptor build() { } + public final class AnonymousAuthenticationSpec { + + private AnonymousAuthenticationSpec() { + } + + protected AnonymousPayloadInterceptor build() { + AnonymousPayloadInterceptor result = new AnonymousPayloadInterceptor("anonymousUser"); + result.setOrder(PayloadInterceptorOrder.ANONYMOUS.getOrder()); + return result; + } + + } + public final class BasicAuthenticationSpec { private ReactiveAuthenticationManager authenticationManager;