Add AuthorizationManager to Messaging

Closes gh-11076
This commit is contained in:
Josh Cummings
2022-04-07 17:39:10 -06:00
parent 495028eb85
commit 8a03d1fcec
31 changed files with 2843 additions and 162 deletions

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2002-2022 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.web.socket;
import java.util.HashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class WebSocketMessageBrokerSecurityConfigurationDocTests {
AnnotationConfigWebApplicationContext context;
TestingAuthenticationToken messageUser;
CsrfToken token;
String sessionAttr;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.sessionAttr = "sessionAttr";
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
@AfterEach
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void securityMappings() {
loadConfig(WebSocketSecurityConfig.class);
clientInboundChannel().send(message("/user/queue/errors", SimpMessageType.SUBSCRIBE));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll", SimpMessageType.MESSAGE)))
.withCauseInstanceOf(AccessDeniedException.class);
}
private void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.register(WebSocketConfig.class, SyncExecutorConfig.class);
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
private MessageChannel clientInboundChannel() {
return this.context.getBean("clientInboundChannel", MessageChannel.class);
}
private Message<String> message(String destination, SimpMessageType type) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type);
return message(headers, destination);
}
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) {
headers.setSessionId("123");
headers.setSessionAttributes(new HashMap<>());
if (destination != null) {
headers.setDestination(destination);
}
if (this.messageUser != null) {
headers.setUser(this.messageUser);
}
return new GenericMessage<>("hi", headers.getMessageHeaders());
}
@Controller
static class MyController {
@MessageMapping("/authentication")
void authentication(@AuthenticationPrincipal String un) {
// ... do something ...
}
}
@Configuration
@EnableWebSocketSecurity
static class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> authorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages.nullDestMatcher().authenticated()
// <1>
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
// <2>
.simpDestMatchers("/app/**").hasRole("USER")
// <3>
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll(); // <6>
return messages.build();
}
}
@Configuration
@EnableWebSocketMessageBroker
static class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
}
@Configuration
static class SyncExecutorConfig {
@Bean
static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
return new SyncExecutorSubscribableChannelPostProcessor();
}
}
}

View File

@@ -0,0 +1,786 @@
/*
* Copyright 2002-2022 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.web.socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.support.AbstractMessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Controller;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeFailureException;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
public class WebSocketMessageBrokerSecurityConfigurationTests {
AnnotationConfigWebApplicationContext context;
TestingAuthenticationToken messageUser;
CsrfToken token;
String sessionAttr;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.sessionAttr = "sessionAttr";
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
@AfterEach
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void simpleRegistryMappings() {
loadConfig(SockJsSecurityConfig.class);
clientInboundChannel().send(message("/permitAll"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void annonymousSupported() {
loadConfig(SockJsSecurityConfig.class);
this.messageUser = null;
clientInboundChannel().send(message("/permitAll"));
}
// gh-3797
@Test
public void beanResolver() {
loadConfig(SockJsSecurityConfig.class);
this.messageUser = null;
clientInboundChannel().send(message("/beanResolver"));
}
@Test
public void addsAuthenticationPrincipalResolver() {
loadConfig(SockJsSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Message<String> message = message("/permitAll/authentication");
messageChannel.send(message);
assertThat(this.context.getBean(MyController.class).authenticationPrincipal)
.isEqualTo((String) this.messageUser.getPrincipal());
}
@Test
public void addsAuthenticationPrincipalResolverWhenNoAuthorization() {
loadConfig(NoInboundSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Message<String> message = message("/permitAll/authentication");
messageChannel.send(message);
assertThat(this.context.getBean(MyController.class).authenticationPrincipal)
.isEqualTo((String) this.messageUser.getPrincipal());
}
@Test
public void addsCsrfProtectionWhenNoAuthorization() {
loadConfig(NoInboundSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MessageChannel messageChannel = clientInboundChannel();
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message))
.withCauseInstanceOf(MissingCsrfTokenException.class);
}
@Test
public void csrfProtectionForConnect() {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MessageChannel messageChannel = clientInboundChannel();
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message))
.withCauseInstanceOf(MissingCsrfTokenException.class);
}
@Test
@Disabled // to be added back in with the introduction of DSL support
public void csrfProtectionDisabledForConnect() {
loadConfig(CsrfDisabledSockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/permitAll/connect");
MessageChannel messageChannel = clientInboundChannel();
messageChannel.send(message);
}
@Test
public void csrfProtectionDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(CsrfChannelInterceptor.class);
}
@Test
public void messagesConnectUseCsrfTokenHandshakeInterceptor() throws Exception {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = sockjsHttpRequest("/chat");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void messagesConnectUseCsrfTokenHandshakeInterceptorMultipleMappings() throws Exception {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = sockjsHttpRequest("/other");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void messagesConnectWebSocketUseCsrfTokenHandshakeInterceptor() throws Exception {
loadConfig(WebSocketSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = websocketHttpRequest("/websocket");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void msmsRegistryCustomPatternMatcher() {
loadConfig(MsmsRegistryCustomPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a.b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a.b.c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void overrideMsmsRegistryCustomPatternMatcher() {
loadConfig(OverrideMsmsRegistryCustomPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a/b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void defaultPatternMatcher() {
loadConfig(DefaultPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a/b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void customExpression() {
loadConfig(CustomExpressionConfig.class);
clientInboundChannel().send(message("/denyRob"));
this.messageUser = new TestingAuthenticationToken("rob", "password", "ROLE_USER");
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyRob")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void channelSecurityInterceptorUsesMetadataSourceBeanWhenProxyingDisabled() {
loadConfig(SockJsProxylessSecurityConfig.class);
AbstractMessageChannel messageChannel = clientInboundChannel();
AuthorizationManager<Message<?>> authorizationManager = this.context.getBean(AuthorizationManager.class);
for (ChannelInterceptor interceptor : messageChannel.getInterceptors()) {
if (interceptor instanceof AuthorizationChannelInterceptor) {
assertThat(ReflectionTestUtils.getField(interceptor, "preSendAuthorizationManager"))
.isSameAs(authorizationManager);
return;
}
}
fail("did not find AuthorizationChannelInterceptor");
}
@Test
public void securityContextChannelInterceptorDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(SecurityContextChannelInterceptor.class);
}
@Test
public void inboundChannelSecurityDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(AuthorizationChannelInterceptor.class);
}
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
}
private HttpRequestHandler handler(HttpServletRequest request) throws Exception {
HandlerMapping handlerMapping = this.context.getBean(HandlerMapping.class);
return (HttpRequestHandler) handlerMapping.getHandler(request).getHandler();
}
private MockHttpServletRequest websocketHttpRequest(String mapping) {
MockHttpServletRequest request = sockjsHttpRequest(mapping);
request.setRequestURI(mapping);
return request;
}
private MockHttpServletRequest sockjsHttpRequest(String mapping) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
request.setMethod("GET");
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
request.setAttribute(CsrfToken.class.getName(), this.token);
return request;
}
private Message<String> message(String destination) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
return message(headers, destination);
}
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) {
headers.setSessionId("123");
headers.setSessionAttributes(new HashMap<>());
if (destination != null) {
headers.setDestination(destination);
}
if (this.messageUser != null) {
headers.setUser(this.messageUser);
}
return new GenericMessage<>("hi", headers.getMessageHeaders());
}
private <T extends MessageChannel> T clientInboundChannel() {
return (T) this.context.getBean("clientInboundChannel", MessageChannel.class);
}
private void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/app/a.*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class OverrideMsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestPathMatcher(new AntPathMatcher())
.simpDestMatchers("/app/a/*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class DefaultPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/app/a/*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class CustomExpressionConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
@Bean
AuthorizationManager<Message<Object>> authorizationManager() {
return (authentication, message) -> {
Authentication auth = authentication.get();
return new AuthorizationDecision(auth != null && !"rob".equals(auth.getName()));
};
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Controller
static class MyController {
String authenticationPrincipal;
MyCustomArgument myCustomArgument;
@MessageMapping("/authentication")
void authentication(@AuthenticationPrincipal String un) {
this.authenticationPrincipal = un;
}
@MessageMapping("/myCustom")
void myCustom(MyCustomArgument myCustomArgument) {
this.myCustomArgument = myCustomArgument;
}
}
static class MyCustomArgument {
MyCustomArgument(String notDefaultConstr) {
}
}
static class MyCustomArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(MyCustomArgument.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) {
return new MyCustomArgument("");
}
}
static class TestHandshakeHandler implements HandshakeHandler {
Map<String, Object> attributes;
@Override
public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws HandshakeFailureException {
this.attributes = attributes;
if (wsHandler instanceof SockJsWebSocketHandler) {
// work around SPR-12716
SockJsWebSocketHandler sockJs = (SockJsWebSocketHandler) wsHandler;
WebSocketServerSockJsSession session = (WebSocketServerSockJsSession) ReflectionTestUtils
.getField(sockJs, "sockJsSession");
this.attributes = session.getAttributes();
}
return true;
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class SockJsSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/other").setHandshakeHandler(testHandshakeHandler())
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
registry.addEndpoint("/chat").setHandshakeHandler(testHandshakeHandler())
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages,
SecurityCheck security) {
AuthorizationManager<MessageAuthorizationContext<?>> beanResolver =
(authentication, context) -> new AuthorizationDecision(security.check());
messages
.simpDestMatchers("/permitAll/**").permitAll()
.simpDestMatchers("/beanResolver/**").access(beanResolver)
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
@Bean
SecurityCheck security() {
return new SecurityCheck();
}
static class SecurityCheck {
private boolean check;
boolean check() {
this.check = !this.check;
return this.check;
}
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class NoInboundSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/other")
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
registry.addEndpoint("/chat")
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
}
@Configuration
@Import(SockJsSecurityConfig.class)
static class CsrfDisabledSockJsSecurityConfig {
@Bean
Consumer<List<ChannelInterceptor>> channelInterceptorCustomizer() {
return (interceptors) -> interceptors.remove(1);
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/websocket")
.setHandshakeHandler(testHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Bean
AuthorizationManager<Message<?>> authorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
// @formatter:off
messages
.simpDestMatchers("/permitAll/**").permitAll()
.anyMessage().denyAll();
// @formatter:on
return messages.build();
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class UsingLegacyConfigurerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/websocket")
.setHandshakeHandler(testHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Override
public void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
// @formatter:off
messages
.simpDestMatchers("/permitAll/**").permitAll()
.anyMessage().denyAll();
// @formatter:on
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class SockJsProxylessSecurityConfig implements WebSocketMessageBrokerConfigurer {
private ApplicationContext context;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/chat")
.setHandshakeHandler(this.context.getBean(TestHandshakeHandler.class))
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Autowired
void setContext(ApplicationContext context) {
this.context = context;
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
static class SyncExecutorConfig {
@Bean
static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
return new SyncExecutorSubscribableChannelPostProcessor();
}
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.security.config.websocket;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;
@@ -33,6 +34,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.MethodParameter;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@@ -44,6 +47,8 @@ import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
@@ -68,6 +73,9 @@ import org.springframework.web.socket.server.HandshakeHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/**
@@ -168,6 +176,78 @@ public class WebSocketMessageBrokerConfigTests {
send(message);
}
@Test
public void sendWhenNoIdSpecifiedThenIntegratesWithAuthorizationManager() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
this.clientInboundChannel.send(message("/permitAll"));
assertThatExceptionOfType(Exception.class).isThrownBy(() -> this.clientInboundChannel.send(message("/denyAll")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenAnonymousMessageWithConnectMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
headers.setNativeHeader(this.token.getHeaderName(), this.token.getToken());
this.clientInboundChannel.send(message("/permitAll", headers));
}
@Test
public void sendWhenAnonymousMessageWithConnectAckMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.CONNECT_ACK);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithDisconnectMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.DISCONNECT);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithDisconnectAckMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.DISCONNECT_ACK);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithHeartbeatMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.HEARTBEAT);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithMessageMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.MESSAGE);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithOtherMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.OTHER);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithSubscribeMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.SUBSCRIBE);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithUnsubscribeMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
send(message);
}
@Test
public void sendWhenConnectWithoutCsrfTokenThenDenied() {
this.spring.configLocations(xml("SyncConfig")).autowire();
@@ -196,6 +276,19 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForMessageTypeThenAuthorizationManagerDeniesOnTypeMismatch() {
this.spring.configLocations(xml("MessageInterceptTypeAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.MESSAGE);
send(message);
message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/anyOther", SimpMessageType.MESSAGE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForSubscribeTypeThenDeniesOnTypeMismatch() {
this.spring.configLocations(xml("SubscribeInterceptTypeConfig")).autowire();
@@ -209,6 +302,19 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForSubscribeTypeThenAuthorizationManagerDeniesOnTypeMismatch() {
this.spring.configLocations(xml("SubscribeInterceptTypeAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.SUBSCRIBE);
send(message);
message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/anyOther", SimpMessageType.SUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void configureWhenUsingConnectMessageTypeThenAutowireFails() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
@@ -309,6 +415,16 @@ public class WebSocketMessageBrokerConfigTests {
send(message);
}
@Test
public void sendWhenUsingCustomPathMatcherThenAuthorizationManagerAppliesIt() {
this.spring.configLocations(xml("CustomPathMatcherAuthorizationManager")).autowire();
Message<?> message = message("/denyAll.a");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/denyAll.a.b");
send(message);
}
@Test
public void sendWhenIdSpecifiedThenSecurityDoesNotIntegrateWithClientInboundChannel() {
this.spring.configLocations(xml("IdConfig")).autowire();
@@ -342,6 +458,27 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(username = "nile")
public void sendWhenCustomExpressionHandlerThenAuthorizationManagerAuthorizesAccordingly() {
this.spring.configLocations(xml("CustomExpressionHandlerAuthorizationManager")).autowire();
Message<?> message = message("/denyNile");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenCustomAuthorizationManagerThenAuthorizesAccordingly() {
this.spring.configLocations(xml("CustomAuthorizationManagerConfig")).autowire();
AuthorizationManager<Message<?>> authorizationManager = this.spring.getContext()
.getBean(AuthorizationManager.class);
given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false));
Message<?> message = message("/any");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
verify(authorizationManager).check(any(), any());
}
private String xml(String configName) {
return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
}
@@ -466,6 +603,17 @@ public class WebSocketMessageBrokerConfigTests {
};
}
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
Message<Object> message) {
return new StandardEvaluationContext(new MessageSecurityExpressionRoot(authentication, message) {
public boolean denyNile() {
Authentication auth = getAuthentication();
return auth != null && !"nile".equals(auth.getName());
}
});
}
}
}