SEC-2861: Add WebSocket Documentation & Sample
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
[[websocket]]
|
||||
== WebSocket Security
|
||||
|
||||
Spring Security 4 added support for securing http://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.
|
||||
|
||||
NOTE: You can find a complete working sample of WebSocket security in samples/chat-jc.
|
||||
|
||||
.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, so there is http://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 rather invasive.
|
||||
****
|
||||
|
||||
[[websocket-configuration]]
|
||||
=== WebSocket Configuration
|
||||
|
||||
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
|
||||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig
|
||||
extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
|
||||
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
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>>
|
||||
|
||||
Spring Security also provides <<nsa-websocket,XML Namespace>> support for securing WebSockets.
|
||||
A comparable XML based configuration looks like the following:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<websocket-message-broker> <!--1--> <!--2-->
|
||||
<!--3-->
|
||||
<intercept-message pattern="/user/**" access="hasRole('USER')" />
|
||||
</websocket-message-broker>
|
||||
----
|
||||
|
||||
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>>
|
||||
|
||||
[[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.
|
||||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry 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>
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any message without a destination (i.e. anything other that 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.
|
||||
|
||||
Spring Security also provides <<nsa-websocket,XML Namespace>> support for securing WebSockets.
|
||||
A comparable XML based configuration looks like the following:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<websocket-message-broker>
|
||||
<!--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/**" access="hasRole('USER')" />
|
||||
<intercept-message pattern="/topic/friends/*" 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 of type CONNECT, UNSUBSCRIBE, or DISCONNECT 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 with a destination is rejected. This is a good idea to ensure that you do not miss any messages.
|
||||
|
||||
[[websocket-authorization-notes]]
|
||||
==== WebSocket Authorization Notes
|
||||
|
||||
In order to properly secure your application it is important to understand Spring's WebSocket support.
|
||||
|
||||
[[websocket-authorization-notes-messagetypes]]
|
||||
===== WebSocket Authorization on Message Types
|
||||
|
||||
It is important to understand the distinction between SUBSCRIBE and MESSAGE types of messages and how it works within Spring.
|
||||
|
||||
Consider a chat application.
|
||||
|
||||
* The system can send notifications 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", then 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 message that starts with the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
|
||||
|
||||
[[websocket-authorization-notes-destinations]]
|
||||
===== WebSocket Authorization on Destinations
|
||||
|
||||
It is also is important to understand how destinations are transformed.
|
||||
|
||||
Consider a chat application.
|
||||
|
||||
* User's can send messages to a specific user by sending a message to the destination of "/app/chat".
|
||||
* 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 using `SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`.
|
||||
* The message gets turned into the destination of "/queue/user/messages-<sessionid>"
|
||||
|
||||
With the application above, we want to allow our client to listen to "/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 allow the client to see messages for every user.
|
||||
|
||||
In general, it is common for applications to deny any SUBSCRIBE sent to a message that starts with the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
|
||||
Of course we may provide exceptions to account for things like
|
||||
|
||||
[[websocket-authorization-notes-outbound]]
|
||||
==== Outbound Messages
|
||||
|
||||
Spring contains a section titled http://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.
|
||||
It is important to note that Spring Security only secures 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, there are typically many many more that go out.
|
||||
Instead of securing the outbound messages, we encourage securing the subscription to the endpoints.
|
||||
|
||||
[[websocket-sameorigin]]
|
||||
=== Enforcing Same Origin Policy
|
||||
|
||||
It is important to emphasize that the browser does not enforce the http://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 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 (i.e. transfer money), evil.com can do on that users behalf.
|
||||
|
||||
Since SockJS tries to emulate WebSockets it also bypasses the Same Origin Policy.
|
||||
This means developers need to explicitly protect their applications from external domains when using SockJS.
|
||||
|
||||
[[websocket-sameorigin-spring]]
|
||||
==== Spring WebSocket Allowed Origin
|
||||
|
||||
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the http://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 http://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29[defence in depth].
|
||||
|
||||
[[websocket-sameorigin-csrf]]
|
||||
==== Adding CSRF to Stomp Headers
|
||||
|
||||
By default Spring Security requires the <<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 <<csrf-include-csrf-token,obtain a CSRF token>> by accessing the request attribute named _csrf.
|
||||
For example, the following will allow accessing the `CsrfToken` in a JSP:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
var headerName = "${_csrf.headerName}";
|
||||
var token = "${_csrf.token}";
|
||||
----
|
||||
|
||||
If you are using static HTML, you can expose the `CsrfToken` on a REST endpoint.
|
||||
For example, the following would expose the `CsrfToken` on the URL /csrf
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
||||
@RequestMapping("/csrf")
|
||||
public CsrfToken csrf(CsrfToken token) {
|
||||
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.
|
||||
For example:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
...
|
||||
var headers = {};
|
||||
headers[headerName] = token;
|
||||
stompClient.connect(headers, function(frame) {
|
||||
...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
[[websocket-sameorigin-disable]]
|
||||
==== Disable CSRF within WebSockets
|
||||
|
||||
If 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:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
protected boolean sameOriginEnforced() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[websocket-sockjs]]
|
||||
=== Working with SockJS
|
||||
|
||||
http://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 an https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport that leverages an iframe].
|
||||
By default Spring Security will <<headers-frame-options,deny>> the site from being framed to prevent Clickjacking attacks.
|
||||
To allow SockJS frame based transports to work, we need to configure Spring Security to allow the same origin to frame the content.
|
||||
|
||||
You can customize X-Frame-Options with the <<nsa-frame-options,frame-options>> element.
|
||||
For example, the following will instruct 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 using the following:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends
|
||||
WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.headers()
|
||||
.frameOptions()
|
||||
.sameOrigin();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[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 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 will be vulnerable to CSRF attacks.
|
||||
|
||||
We can easily achieve this by providing a CSRF RequestMatcher.
|
||||
Our Java Configuration makes this extremely easy.
|
||||
For example, if our stomp endpoint is "/chat" we can disable CSRF protection for only URLs that start with "/chat/" using the following configuration:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig
|
||||
extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
|
||||
http
|
||||
.csrf()
|
||||
// ignore our stomp endpoints since they are protected using Stomp headers
|
||||
.ignoringAntMatchers("/chat/**")
|
||||
.and()
|
||||
.headers()
|
||||
// allow same origin to frame our site to support iframe SockJS
|
||||
.frameOptions().sameOrigin()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
|
||||
...
|
||||
----
|
||||
|
||||
If we are using XML based configuration, we can use the <<nsa-csrf-request-matcher-ref>>.
|
||||
For example:
|
||||
|
||||
[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_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>
|
||||
----
|
||||
@@ -4065,6 +4065,7 @@ Rounding out the anonymous authentication discussion is the `AuthenticationTrust
|
||||
|
||||
You will often see the `ROLE_ANONYMOUS` attribute in the above interceptor configuration replaced with `IS_AUTHENTICATED_ANONYMOUSLY`, which is effectively the same thing when defining access controls. This is an example of the use of the `AuthenticatedVoter` which we will see in the <<authz-authenticated-voter,authorization chapter>>. It uses an `AuthenticationTrustResolver` to process this particular configuration attribute and grant access to anonymous users. the `AuthenticatedVoter` approach is more powerful, since it allows you to differentiate between anonymous, remember-me and fully-authenticated users. If you don't need this functionality though, then you can stick with `ROLE_ANONYMOUS`, which will be processed by Spring Security's standard `RoleVoter`.
|
||||
|
||||
include::{include-dir}/websocket.adoc[]
|
||||
|
||||
[[authorization]]
|
||||
= Authorization
|
||||
|
||||
Reference in New Issue
Block a user