mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-11-04 00:28:54 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			792 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			792 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
[[websocket]]
 | 
						|
= WebSocket Security
 | 
						|
 | 
						|
Spring Security 4 added support for securing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support].
 | 
						|
This section describes how to use Spring Security's WebSocket support.
 | 
						|
 | 
						|
.Direct JSR-356 Support
 | 
						|
****
 | 
						|
Spring Security does not provide direct JSR-356 support, because doing so would provide little value.
 | 
						|
This is because the format is unknown, and there is https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-intro-sub-protocol[little Spring can do to secure an unknown format].
 | 
						|
Additionally, JSR-356 does not provide a way to intercept messages, so security would be invasive.
 | 
						|
****
 | 
						|
 | 
						|
[[websocket-authentication]]
 | 
						|
== WebSocket Authentication
 | 
						|
 | 
						|
WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
 | 
						|
This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets.
 | 
						|
If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
 | 
						|
 | 
						|
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
 | 
						|
 | 
						|
[[websocket-authorization]]
 | 
						|
== WebSocket Authorization
 | 
						|
 | 
						|
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
 | 
						|
 | 
						|
In Spring Security 5.8, this support has been refreshed to use the `AuthorizationManager` API.
 | 
						|
 | 
						|
To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager<Message<?>>` bean or in xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML] use the `use-authorization-manager` attribute.
 | 
						|
One way to do this is by using the `AuthorizationManagerMessageMatcherRegistry` to specify endpoint patterns like so:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSocketSecurity // <1> <2>
 | 
						|
public class WebSocketSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
 | 
						|
        messages
 | 
						|
                .simpDestMatchers("/user/**").hasRole("USER") // <3>
 | 
						|
 | 
						|
        return messages.build();
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSocketSecurity // <1> <2>
 | 
						|
open class WebSocketSecurityConfig { // <1> <2>
 | 
						|
    @Bean
 | 
						|
    fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
 | 
						|
        messages.simpDestMatchers("/user/**").hasRole("USER") // <3>
 | 
						|
        return messages.build()
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Xml::
 | 
						|
+
 | 
						|
[source,xml,role="secondary"]
 | 
						|
----
 | 
						|
<websocket-message-broker use-authorization-manager="true"> <1> <2>
 | 
						|
    <intercept-message pattern="/user/**" access="hasRole('USER')"/> <3>
 | 
						|
</websocket-message-broker>
 | 
						|
----
 | 
						|
======
 | 
						|
<1> Any inbound CONNECT message requires a valid CSRF token to enforce the <<websocket-sameorigin,Same Origin Policy>>.
 | 
						|
<2> The `SecurityContextHolder` is populated with the user within the `simpUser` header attribute for any inbound request.
 | 
						|
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with `/user/` will require `ROLE_USER`. You can find additional details on authorization in <<websocket-authorization>>
 | 
						|
 | 
						|
=== Custom Authorization
 | 
						|
 | 
						|
When using `AuthorizationManager`, customization is quite simple.
 | 
						|
For example, you can publish an `AuthorizationManager` that requires that all messages have a role of "USER" using `AuthorityAuthorizationManager`, as seen below:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSocketSecurity // <1> <2>
 | 
						|
public class WebSocketSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
 | 
						|
        return AuthorityAuthorizationManager.hasRole("USER");
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSocketSecurity // <1> <2>
 | 
						|
open class WebSocketSecurityConfig {
 | 
						|
    @Bean
 | 
						|
    fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
 | 
						|
        return AuthorityAuthorizationManager.hasRole("USER") // <3>
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Xml::
 | 
						|
+
 | 
						|
[source,xml,role="secondary"]
 | 
						|
----
 | 
						|
<bean id="authorizationManager" class="org.example.MyAuthorizationManager"/>
 | 
						|
 | 
						|
<websocket-message-broker authorization-manager-ref="myAuthorizationManager"/>
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
There are several ways to further match messages, as can be seen in a more advanced example below:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
public class WebSocketSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    public AuthorizationManager<Message<?>> messageAuthorizationManager(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(MESSAGE, SUBSCRIBE).denyAll() // <5>
 | 
						|
                .anyMessage().denyAll(); // <6>
 | 
						|
 | 
						|
        return messages.build();
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
open class WebSocketSecurityConfig {
 | 
						|
    fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<*>> {
 | 
						|
        messages
 | 
						|
            .nullDestMatcher().authenticated() // <1>
 | 
						|
            .simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
 | 
						|
            .simpDestMatchers("/app/**").hasRole("USER") // <3>
 | 
						|
            .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
 | 
						|
            .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
 | 
						|
            .anyMessage().denyAll() // <6>
 | 
						|
 | 
						|
        return messages.build();
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Xml::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
<websocket-message-broker use-authorization-manager="true">
 | 
						|
    <!--1-->
 | 
						|
    <intercept-message type="CONNECT" access="permitAll" />
 | 
						|
    <intercept-message type="UNSUBSCRIBE" access="permitAll" />
 | 
						|
    <intercept-message type="DISCONNECT" access="permitAll" />
 | 
						|
 | 
						|
    <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> <!--2-->
 | 
						|
    <intercept-message pattern="/app/**" access="hasRole('USER')" />      <!--3-->
 | 
						|
 | 
						|
    <!--4-->
 | 
						|
    <intercept-message pattern="/user/**" type="SUBSCRIBE" access="hasRole('USER')" />
 | 
						|
    <intercept-message pattern="/topic/friends/*" type="SUBSCRIBE" access="hasRole('USER')" />
 | 
						|
 | 
						|
    <!--5-->
 | 
						|
    <intercept-message type="MESSAGE" access="denyAll" />
 | 
						|
    <intercept-message type="SUBSCRIBE" access="denyAll" />
 | 
						|
 | 
						|
    <intercept-message pattern="/**" access="denyAll" /> <!--6-->
 | 
						|
</websocket-message-broker>
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
This will ensure that:
 | 
						|
 | 
						|
<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
 | 
						|
<2> Anyone can subscribe to /user/queue/errors
 | 
						|
<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
 | 
						|
<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
 | 
						|
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
 | 
						|
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
 | 
						|
 | 
						|
[[migrating-spel-expressions]]
 | 
						|
=== Migrating SpEL Expressions
 | 
						|
 | 
						|
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
 | 
						|
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
 | 
						|
 | 
						|
However, to ease migration, you can also use a class like the following:
 | 
						|
 | 
						|
[source,java]
 | 
						|
----
 | 
						|
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
 | 
						|
 | 
						|
	private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
 | 
						|
 | 
						|
	private Expression expression;
 | 
						|
 | 
						|
	public MessageExpressionAuthorizationManager(String expressionString) {
 | 
						|
		Assert.hasText(expressionString, "expressionString cannot be empty");
 | 
						|
		this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
 | 
						|
	}
 | 
						|
 | 
						|
	@Override
 | 
						|
	public AuthorizationDecision check(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
 | 
						|
		EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
 | 
						|
		boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
 | 
						|
		return new ExpressionAuthorizationDecision(granted, this.expression);
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
And specify an instance for each matcher that you cannot get migrate:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
public class WebSocketSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
 | 
						|
        messages
 | 
						|
                // ...
 | 
						|
                .simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
 | 
						|
                // ...
 | 
						|
 | 
						|
        return messages.build();
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
open class WebSocketSecurityConfig {
 | 
						|
    fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
 | 
						|
        messages
 | 
						|
            // ..
 | 
						|
            .simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
 | 
						|
            // ...
 | 
						|
 | 
						|
        return messages.build()
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
[[websocket-authorization-notes]]
 | 
						|
=== WebSocket Authorization Notes
 | 
						|
 | 
						|
To properly secure your application, you need to understand Spring's WebSocket support.
 | 
						|
 | 
						|
[[websocket-authorization-notes-messagetypes]]
 | 
						|
==== WebSocket Authorization on Message Types
 | 
						|
 | 
						|
You need to understand the distinction between `SUBSCRIBE` and `MESSAGE` types of messages and how they work within Spring.
 | 
						|
 | 
						|
Consider a chat application:
 | 
						|
 | 
						|
* The system can send a notification `MESSAGE` to all users through a destination of `/topic/system/notifications`.
 | 
						|
* Clients can receive notifications by `SUBSCRIBE` to the `/topic/system/notifications`.
 | 
						|
 | 
						|
While we want clients to be able to `SUBSCRIBE` to `/topic/system/notifications`, we do not want to enable them to send a `MESSAGE` to that destination.
 | 
						|
If we allowed sending a `MESSAGE` to `/topic/system/notifications`, clients could send a message directly to that endpoint and impersonate the system.
 | 
						|
 | 
						|
In general, it is common for applications to deny any `MESSAGE` sent to a destination that starts with the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (`/topic/` or `/queue/`).
 | 
						|
 | 
						|
[[websocket-authorization-notes-destinations]]
 | 
						|
==== WebSocket Authorization on Destinations
 | 
						|
 | 
						|
You should also understand how destinations are transformed.
 | 
						|
 | 
						|
Consider a chat application:
 | 
						|
 | 
						|
* Users can send messages to a specific user by sending a message to the `/app/chat` destination.
 | 
						|
* The application sees the message, ensures that the `from` attribute is specified as the current user (we cannot trust the client).
 | 
						|
* The application then sends the message to the recipient by using `SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`.
 | 
						|
* The message gets turned into the destination of `/queue/user/messages-<sessionid>`.
 | 
						|
 | 
						|
With this chat application, we want to let our client to listen `/user/queue`, which is transformed into `/queue/user/messages-<sessionid>`.
 | 
						|
However, we do not want the client to be able to listen to `/queue/*`, because that would let the client see messages for every user.
 | 
						|
 | 
						|
In general, it is common for applications to deny any `SUBSCRIBE` sent to a message that starts with the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (`/topic/` or `/queue/`).
 | 
						|
We may provide exceptions to account for things like
 | 
						|
//FIXME: Like what?
 | 
						|
 | 
						|
[[websocket-authorization-notes-outbound]]
 | 
						|
=== Outbound Messages
 | 
						|
 | 
						|
The Spring Framework reference documentation contains a section titled https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow["`Flow of Messages`"] that describes how messages flow through the system.
 | 
						|
Note that Spring Security secures only the `clientInboundChannel`.
 | 
						|
Spring Security does not attempt to secure the `clientOutboundChannel`.
 | 
						|
 | 
						|
The most important reason for this is performance.
 | 
						|
For every message that goes in, typically many more go out.
 | 
						|
Instead of securing the outbound messages, we encourage securing the subscription to the endpoints.
 | 
						|
 | 
						|
[[websocket-sameorigin]]
 | 
						|
== Enforcing Same Origin Policy
 | 
						|
 | 
						|
Note that the browser does not enforce the https://en.wikipedia.org/wiki/Same-origin_policy[Same Origin Policy] for WebSocket connections.
 | 
						|
This is an extremely important consideration.
 | 
						|
 | 
						|
[[websocket-sameorigin-why]]
 | 
						|
=== Why Same Origin?
 | 
						|
 | 
						|
Consider the following scenario.
 | 
						|
A user visits `bank.com` and authenticates to their account.
 | 
						|
The same user opens another tab in their browser and visits `evil.com`.
 | 
						|
The Same Origin Policy ensures that `evil.com` cannot read data from or write data to `bank.com`.
 | 
						|
 | 
						|
With WebSockets, the Same Origin Policy does not apply.
 | 
						|
In fact, unless `bank.com` explicitly forbids it, `evil.com` can read and write data on behalf of the user.
 | 
						|
This means that anything the user can do over the webSocket (such as transferring money), `evil.com` can do on that user's behalf.
 | 
						|
 | 
						|
Since SockJS tries to emulate WebSockets, it also bypasses the Same Origin Policy.
 | 
						|
This means that developers need to explicitly protect their applications from external domains when they use SockJS.
 | 
						|
 | 
						|
[[websocket-sameorigin-spring]]
 | 
						|
=== Spring WebSocket Allowed Origin
 | 
						|
 | 
						|
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins[current domain].
 | 
						|
Spring Security adds an additional layer of protection to provide https://en.wikipedia.org/wiki/Defence_in_depth_(non-military)#Information_security[defense in depth].
 | 
						|
 | 
						|
[[websocket-sameorigin-csrf]]
 | 
						|
=== Adding CSRF to Stomp Headers
 | 
						|
 | 
						|
By default, Spring Security requires the xref:features/exploits/csrf.adoc#csrf[CSRF token]  in any `CONNECT` message type.
 | 
						|
This ensures that only a site that has access to the CSRF token can connect.
 | 
						|
Since only the *same origin* can access the CSRF token, external domains are not allowed to make a connection.
 | 
						|
 | 
						|
Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
 | 
						|
However, SockJS does not allow for these options.
 | 
						|
Instead, we must include the token in the Stomp headers.
 | 
						|
 | 
						|
Applications can xref:servlet/exploits/csrf.adoc#csrf-integration[obtain a CSRF token] by accessing the request attribute named `_csrf`.
 | 
						|
For example, the following allows accessing the `CsrfToken` in a JSP:
 | 
						|
 | 
						|
[source,javascript]
 | 
						|
----
 | 
						|
var headerName = "${_csrf.headerName}";
 | 
						|
var token = "${_csrf.token}";
 | 
						|
----
 | 
						|
 | 
						|
If you use static HTML, you can expose the `CsrfToken` on a REST endpoint.
 | 
						|
For example, the following would expose the `CsrfToken` on the `/csrf` URL:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@RestController
 | 
						|
public class CsrfController {
 | 
						|
 | 
						|
    @RequestMapping("/csrf")
 | 
						|
    public CsrfToken csrf(CsrfToken token) {
 | 
						|
        return token;
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@RestController
 | 
						|
class CsrfController {
 | 
						|
    @RequestMapping("/csrf")
 | 
						|
    fun csrf(token: CsrfToken): CsrfToken {
 | 
						|
        return token
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
The JavaScript can make a REST call to the endpoint and use the response to populate the `headerName` and the token.
 | 
						|
 | 
						|
We can now include the token in our Stomp client:
 | 
						|
 | 
						|
[source,javascript]
 | 
						|
----
 | 
						|
...
 | 
						|
var headers = {};
 | 
						|
headers[headerName] = token;
 | 
						|
stompClient.connect(headers, function(frame) {
 | 
						|
  ...
 | 
						|
 | 
						|
})
 | 
						|
----
 | 
						|
 | 
						|
[[websocket-sameorigin-disable]]
 | 
						|
=== Disable CSRF within WebSockets
 | 
						|
NOTE: At this point, CSRF is not configurable when using `@EnableWebSocketSecurity`, though this will likely be added in a future release.
 | 
						|
 | 
						|
To disable CSRF, instead of using `@EnableWebSocketSecurity`, you can use XML support or add the Spring Security components yourself, like so:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
 | 
						|
 | 
						|
    private final ApplicationContext applicationContext;
 | 
						|
 | 
						|
    private final AuthorizationManager<Message<?>> authorizationManager;
 | 
						|
    
 | 
						|
    public WebSocketSecurityConfig(ApplicationContext applicationContext, AuthorizationManager<Message<?>> authorizationManager) {
 | 
						|
        this.applicationContext = applicationContext;
 | 
						|
        this.authorizationManager = authorizationManager;
 | 
						|
    }
 | 
						|
 | 
						|
    @Override
 | 
						|
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
 | 
						|
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
 | 
						|
    }
 | 
						|
 | 
						|
    @Override
 | 
						|
    public void configureClientInboundChannel(ChannelRegistration registration) {
 | 
						|
        AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(authorizationManager);
 | 
						|
        AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(applicationContext);
 | 
						|
        authz.setAuthorizationEventPublisher(publisher);
 | 
						|
        registration.interceptors(new SecurityContextChannelInterceptor(), authz);
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
open class WebSocketSecurityConfig(val applicationContext: ApplicationContext, val authorizationManager: AuthorizationManager<Message<*>>) : WebSocketMessageBrokerConfigurer {
 | 
						|
    @Override
 | 
						|
    override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
 | 
						|
        argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
 | 
						|
    }
 | 
						|
 | 
						|
    @Override
 | 
						|
    override fun configureClientInboundChannel(registration: ChannelRegistration) {
 | 
						|
        var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(authorizationManager)
 | 
						|
        var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(applicationContext)
 | 
						|
        authz.setAuthorizationEventPublisher(publisher)
 | 
						|
        registration.interceptors(SecurityContextChannelInterceptor(), authz)
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Xml::
 | 
						|
+
 | 
						|
[source,xml,role="secondary"]
 | 
						|
----
 | 
						|
<websocket-message-broker use-authorization-manager="true" same-origin-disabled="true">
 | 
						|
    <intercept-message pattern="/**" access="authenticated"/>
 | 
						|
</websocket-message-broker>
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
On the other hand, if you are using the <<legacy-websocket-configuration,legacy `AbstractSecurityWebSocketMessageBrokerConfigurer`>> and you want to allow other domains to access your site, you can disable Spring Security's protection.
 | 
						|
For example, in Java Configuration you can use the following:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
 | 
						|
 | 
						|
    ...
 | 
						|
 | 
						|
    @Override
 | 
						|
    protected boolean sameOriginDisabled() {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
 | 
						|
 | 
						|
    // ...
 | 
						|
 | 
						|
    override fun sameOriginDisabled(): Boolean {
 | 
						|
        return true
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
[[websocket-expression-handler]]
 | 
						|
=== Custom Expression Handler
 | 
						|
 | 
						|
At times, there may be value in customizing how the `access` expressions are handled defined in your `intercept-message` XML elements.
 | 
						|
To do this, you can create a class of type `SecurityExpressionHandler<MessageAuthorizationContext<?>>` and refer to it in your XML definition like so:
 | 
						|
 | 
						|
[source,xml]
 | 
						|
----
 | 
						|
<websocket-message-broker use-authorization-manager="true">
 | 
						|
    <expression-handler ref="myRef"/>
 | 
						|
    ...
 | 
						|
</websocket-message-broker>
 | 
						|
 | 
						|
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler"/>
 | 
						|
----
 | 
						|
 | 
						|
If you are migrating from a legacy usage of `websocket-message-broker` that implements a `SecurityExpressionHandler<Message<?>>`, you can:
 | 
						|
 1. Additionally implement the `createEvaluationContext(Supplier, Message)` method and then
 | 
						|
 2. Wrap that value in a `MessageAuthorizationContextSecurityExpressionHandler` like so:
 | 
						|
 | 
						|
[source,xml]
 | 
						|
----
 | 
						|
<websocket-message-broker use-authorization-manager="true">
 | 
						|
    <expression-handler ref="myRef"/>
 | 
						|
    ...
 | 
						|
</websocket-message-broker>
 | 
						|
 | 
						|
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
 | 
						|
    <b:constructor-arg>
 | 
						|
        <b:bean class="org.example.MyLegacyExpressionHandler"/>
 | 
						|
    </b:constructor-arg>
 | 
						|
</b:bean>
 | 
						|
----
 | 
						|
 | 
						|
[[websocket-sockjs]]
 | 
						|
== Working with SockJS
 | 
						|
 | 
						|
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback[SockJS] provides fallback transports to support older browsers.
 | 
						|
When using the fallback options, we need to relax a few security constraints to allow SockJS to work with Spring Security.
 | 
						|
 | 
						|
[[websocket-sockjs-sameorigin]]
 | 
						|
=== SockJS & frame-options
 | 
						|
 | 
						|
SockJS may use a https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport that leverages an iframe].
 | 
						|
By default, Spring Security xref:features/exploits/headers.adoc#headers-frame-options[denies] the site from being framed to prevent clickjacking attacks.
 | 
						|
To allow SockJS frame-based transports to work, we need to configure Spring Security to let the same origin frame the content.
 | 
						|
 | 
						|
You can customize `X-Frame-Options` with the xref:servlet/appendix/namespace/http.adoc#nsa-frame-options[frame-options] element.
 | 
						|
For example, the following instructs Spring Security to use `X-Frame-Options: SAMEORIGIN`, which allows iframes within the same domain:
 | 
						|
 | 
						|
[source,xml]
 | 
						|
----
 | 
						|
<http>
 | 
						|
    <!-- ... -->
 | 
						|
 | 
						|
    <headers>
 | 
						|
        <frame-options
 | 
						|
          policy="SAMEORIGIN" />
 | 
						|
    </headers>
 | 
						|
</http>
 | 
						|
----
 | 
						|
 | 
						|
Similarly, you can customize frame options to use the same origin within Java Configuration by using the following:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSecurity
 | 
						|
public class WebSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 | 
						|
        http
 | 
						|
            // ...
 | 
						|
            .headers(headers -> headers
 | 
						|
                .frameOptions(frameOptions -> frameOptions
 | 
						|
                     .sameOrigin()
 | 
						|
                )
 | 
						|
        );
 | 
						|
        return http.build();
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSecurity
 | 
						|
open class WebSecurityConfig {
 | 
						|
    @Bean
 | 
						|
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
 | 
						|
        http {
 | 
						|
            // ...
 | 
						|
            headers {
 | 
						|
                frameOptions {
 | 
						|
                    sameOrigin = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return http.build()
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
[[websocket-sockjs-csrf]]
 | 
						|
=== SockJS & Relaxing CSRF
 | 
						|
 | 
						|
SockJS uses a POST on the CONNECT messages for any HTTP-based transport.
 | 
						|
Typically, we need to include the CSRF token in an HTTP header or an HTTP parameter.
 | 
						|
However, SockJS does not allow for these options.
 | 
						|
Instead, we must include the token in the Stomp headers as described in <<websocket-sameorigin-csrf>>.
 | 
						|
 | 
						|
It also means that we need to relax our CSRF protection with the web layer.
 | 
						|
Specifically, we want to disable CSRF protection for our connect URLs.
 | 
						|
We do NOT want to disable CSRF protection for every URL.
 | 
						|
Otherwise, our site is vulnerable to CSRF attacks.
 | 
						|
 | 
						|
We can easily achieve this by providing a CSRF `RequestMatcher`.
 | 
						|
Our Java configuration makes this easy.
 | 
						|
For example, if our stomp endpoint is `/chat`, we can disable CSRF protection only for URLs that start with `/chat/` by using the following configuration:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSecurity
 | 
						|
public class WebSecurityConfig {
 | 
						|
 | 
						|
    @Bean
 | 
						|
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 | 
						|
        http
 | 
						|
            .csrf(csrf -> csrf
 | 
						|
                // ignore our stomp endpoints since they are protected using Stomp headers
 | 
						|
                .ignoringRequestMatchers("/chat/**")
 | 
						|
            )
 | 
						|
            .headers(headers -> headers
 | 
						|
                // allow same origin to frame our site to support iframe SockJS
 | 
						|
                .frameOptions(frameOptions -> frameOptions
 | 
						|
                    .sameOrigin()
 | 
						|
                )
 | 
						|
            )
 | 
						|
            .authorizeHttpRequests(authorize -> authorize
 | 
						|
                ...
 | 
						|
            )
 | 
						|
            ...
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
@EnableWebSecurity
 | 
						|
open class WebSecurityConfig {
 | 
						|
    @Bean
 | 
						|
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
 | 
						|
        http {
 | 
						|
            csrf {
 | 
						|
                ignoringRequestMatchers("/chat/**")
 | 
						|
            }
 | 
						|
            headers {
 | 
						|
                frameOptions {
 | 
						|
                    sameOrigin = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            authorizeRequests {
 | 
						|
                // ...
 | 
						|
            }
 | 
						|
            // ...
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
If we use XML-based configuration, we can use thexref:servlet/appendix/namespace/http.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref].
 | 
						|
 | 
						|
[source,xml]
 | 
						|
----
 | 
						|
<http ...>
 | 
						|
    <csrf request-matcher-ref="csrfMatcher"/>
 | 
						|
 | 
						|
    <headers>
 | 
						|
        <frame-options policy="SAMEORIGIN"/>
 | 
						|
    </headers>
 | 
						|
 | 
						|
    ...
 | 
						|
</http>
 | 
						|
 | 
						|
<b:bean id="csrfMatcher"
 | 
						|
    class="AndRequestMatcher">
 | 
						|
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
 | 
						|
    <b:constructor-arg>
 | 
						|
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
 | 
						|
          <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
 | 
						|
            <b:constructor-arg value="/chat/**"/>
 | 
						|
          </b:bean>
 | 
						|
        </b:bean>
 | 
						|
    </b:constructor-arg>
 | 
						|
</b:bean>
 | 
						|
----
 | 
						|
 | 
						|
[[legacy-websocket-configuration]]
 | 
						|
== Legacy WebSocket Configuration
 | 
						|
 | 
						|
Before Spring Security 5.8, the way to configure messaging authorization using Java Configuration, was to extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
 | 
						|
For example:
 | 
						|
 | 
						|
[tabs]
 | 
						|
======
 | 
						|
Java::
 | 
						|
+
 | 
						|
[source,java,role="primary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
public class WebSocketSecurityConfig
 | 
						|
      extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
 | 
						|
 | 
						|
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
 | 
						|
        messages
 | 
						|
                .simpDestMatchers("/user/**").authenticated() // <3>
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
 | 
						|
Kotlin::
 | 
						|
+
 | 
						|
[source,kotlin,role="secondary"]
 | 
						|
----
 | 
						|
@Configuration
 | 
						|
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
 | 
						|
    override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
 | 
						|
        messages.simpDestMatchers("/user/**").authenticated() // <3>
 | 
						|
    }
 | 
						|
}
 | 
						|
----
 | 
						|
======
 | 
						|
 | 
						|
This will ensure that:
 | 
						|
 | 
						|
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
 | 
						|
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
 | 
						|
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
 | 
						|
 | 
						|
Using the legacy configuration is helpful in the event that you have a custom `SecurityExpressionHandler` that extends `AbstractSecurityExpressionHandler` and overrides `createEvaluationContextInternal` or `createSecurityExpressionRoot`.
 | 
						|
In order to defer `Authorization` lookup, the new `AuthorizationManager` API does not invoke these when evaluating expressions.
 | 
						|
 | 
						|
If you are using XML, you can use the legacy APIs simply by not using the `use-authorization-manager` element or setting it to `false`.
 |