76

I've been struggling a lot to properly implement Stomp (websocket) Authentication and Authorization with Spring-Security. For posterity i'll answer my own question to provide a guide.


The Problem

Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authentication and Authorization.


What i want

  • Authenticate users with login/password.
  • Prevent anonymous users to CONNECT though WebSocket.
  • Add authorization layer (user, admin, ...).
  • Having Principal available in controllers.

What i don't want

  • Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
Community
  • 1
  • 1
Anthony Raymond
  • 7,434
  • 6
  • 42
  • 59
  • Great write-up. Is it a worse approach, to actually defer authentication even further, to the handling of the first SEND frame? as opposed to the CONNECT frame. I am not yet clear on any benefits it might yield, but is it perhaps a no-no, compared to the way you describe in the answer? – almondandapricot Jan 25 '21 at 13:49
  • ...first SEND or SUBSCRIBE, in fact – almondandapricot Jan 25 '21 at 13:57
  • IMHO it's better to do it on CONNECT (and STOMP to support v1.2), because it's a common entrypoint instead of SEND, SUBSCRIBE, BEGIN, or any other Frame that may be introduce in future RFC – Anthony Raymond Jan 26 '21 at 12:11

4 Answers4

133

As stated above the documentation looks unclear (IMHO), until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.

A really nice attempt was made by Rob-Leggett but, he was forking some Springs class and I don't feel comfortable doing so.

Things to know before you start:

  • Security chain and Security config for http and WebSocket are completely independent.
  • Spring AuthenticationProvider take not part at all in Websocket authentication.
  • The authentication won't happen on HTTP negotiation endpoint in our case, because none of the JavaScripts STOMP (websocket) libraries I know sends the necessary authentication headers along with the HTTP request.
  • Once set on CONNECT request, the user (simpUser) will be stored in the websocket session and no more authentication will be required on further messages.

Maven deps

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

WebSocket configuration

The below config register a simple message broker (a simple endpoint that we will later protect).

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(final MessageBrokerRegistry config) {
        // These are endpoints the client can subscribes to.
        config.enableSimpleBroker("/queue/topic");
        // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        // Handshake endpoint
        registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
    }
}

Spring security config

Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        // This is not for websocket authorization, and this should most likely not be altered.
        http
                .httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests().antMatchers("/stomp").permitAll()
                .anyRequest().denyAll();
    }
}

Then we'll create a service responsible for authenticating users.
@Component
public class WebSocketAuthenticatorService {
    // This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
    public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String  username, final String password) throws AuthenticationException {
        if (username == null || username.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
        }
        if (password == null || password.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
        }
        // Add your own logic for retrieving user in fetchUserFromDb()
        if (fetchUserFromDb(username, password) == null) {
            throw new BadCredentialsException("Bad credentials for user " + username);
        }

        // null credentials, we do not pass the password along
        return new UsernamePasswordAuthenticationToken(
                username,
                null,
                Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
        );
    }
}

Note that: UsernamePasswordAuthenticationToken MUST have at least one GrantedAuthority, if you use another constructor, Spring will auto-set isAuthenticated = false.


Almost there, now we need to create an Interceptor that will set the `simpUser` header or throw `AuthenticationException` on CONNECT messages.
@Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
    private static final String USERNAME_HEADER = "login";
    private static final String PASSWORD_HEADER = "passcode";
    private final WebSocketAuthenticatorService webSocketAuthenticatorService;

    @Inject
    public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
        this.webSocketAuthenticatorService = webSocketAuthenticatorService;
    }

    @Override
    public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
        final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        if (StompCommand.CONNECT == accessor.getCommand()) {
            final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
            final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);

            final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);

            accessor.setUser(user);
        }
        return message;
    }
}

Note that: preSend() MUST return a UsernamePasswordAuthenticationToken, another element in the spring security chain test this. Note that: If your UsernamePasswordAuthenticationToken was built without passing GrantedAuthority, the authentication will fail, because the constructor without granted authorities auto set authenticated = false THIS IS AN IMPORTANT DETAIL which is not documented in spring-security.


Finally create two more class to handle respectively Authorization and Authentication.
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationSecurityConfig extends  WebSocketMessageBrokerConfigurer {
    @Inject
    private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
    
    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        // Endpoints are already registered on WebSocketConfig, no need to add more.
    }

    @Override
    public void configureClientInboundChannel(final ChannelRegistration registration) {
        registration.setInterceptors(authChannelInterceptorAdapter);
    }

}

Note that: The @Order is CRUCIAL don't forget it, it allows our interceptor to be registered first in the security chain.

@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
        // You can customize your authorization mapping here.
        messages.anyMessage().authenticated();
    }

    // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}
Anthony Raymond
  • 7,434
  • 6
  • 42
  • 59
  • Don't quite understand - what's wrong with the [Spring documentation](https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html) on this? Why aren't you using `AbstractSecurityWebSocketMessageBrokerConfigurer`? As it stands you have no CSRF. Also, I sincerely hope your passwords are stored hashed in the database... – Boris the Spider Jul 30 '17 at 22:41
  • i do use `AbstractSecurityWebSocketMessageBrokerConfigurer` (take a look at the last code block) but this class is just for Authorization, you just can't register a Interceptor before the security check and thus fill your `simpUser`. To register the interceptor before the security check you have to use `AbstractWebSocketMessageBrokerConfigurer `with a super high `@Order`. Plus the Interceptor must fill the `simpUser` with a `UsernamePasswordAuthenticationToken` and nothing else, which is not documented at all. – Anthony Raymond Jul 30 '17 at 22:46
  • *As it stands you have no CSRF. Also, I sincerely hope your passwords are stored hashed in the database*. Woops, i disabled CSRF for test purpose and forgot to remove this line. - I personaly don't use database, there is only one user allowed to connect, and the password is defined at runtime. – Anthony Raymond Jul 30 '17 at 23:14
  • 1
    Quoting from the [Spring Security guide](https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html) "_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._" So, the point is, that you authenticate access to the `http` endpoint using standard Spring Security methods, then you verify CSRF on `CONNECT` and use role based security on configured STOMP destinations. I am still unsure of the use case for the above. – Boris the Spider Aug 01 '17 at 06:43
  • 3
    The fact is that none of the javascrip STOMP libraries are passing authentication headers along with the HTTP handshake call. And Spring has chosen to allow users to authenticate only though HTTP. But we cant blame them for that, the WebSocket RFC is unclear and very permissive on this subject: *This protocol doesn't prescribe any particular way that servers can authenticate clients during the WebSocket handshake.* – Anthony Raymond Aug 01 '17 at 10:01
  • But if only an authenticated user can _access_ the websocket endpoint then reauthenticating them on `CONNECT` is a little pointless. No? – Boris the Spider Aug 01 '17 at 10:12
  • 1
    None of JavaScript stomp library send authentication headers along with http handshake. So you can't restrict access to the http negociation endpoint. You have to authenticate your users on CONNECT. Of course if you use a spring client you won't need all of this overwelmed stuff. The point of this boilerplate is to provide an example that use Spring backend for JavaScript stomp clients. – Anthony Raymond Aug 01 '17 at 11:29
  • But to _connect_ to the websocket, you need access to the URL. _That_ is where the Spring Security authentication kicks in. Then the role based authorisation ensures that the authenticated user can only subscribe to the appropriate locations. I really don't see what adding credentials to `CONNECT` adds, as that user is _already authenticated_ during access to the URL. – Boris the Spider Aug 01 '17 at 11:35
  • 5
    Spring described method state that : *You should provide sufficient information (login password or whatever) while accessing the HTTP necotiation endpoint (handshake endpoint) to allow Spring to authenticate you through the Spring-Security chain*. But none of the javaScript STOMP libraries do send these information along with the HTTP negiciation call. These headers are sent with `CONNECT` message in `nativeHeaders: { login: xxxx, passcode: xxxx}`. Therefore if no informations are sent on HTTP call, you can't authenticate at this point. – Anthony Raymond Aug 01 '17 at 11:49
  • I want to implement machine to backend web socket authentication and authorization, there are no user Principle, then how can I proceed? – Amit Aug 31 '17 at 04:59
  • I'm sorry but i don't undestand your question – Anthony Raymond Aug 31 '17 at 07:10
  • @AnthonyRaymond I'm getting `message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.web.csrf.MissingCsrfTokenException\c Could not verify the provided CSRF token because your session was not found. content-length:0`. Only changes I made are: `1.)` removed `sameOriginDisabled` `2.)` left `Collections.singleton((GrantedAuthority) () -> "USER"));` but I'm not quite sure what it supposed to do. I have different roles for users if this is it. F.e. I defined `USERS_READ`, `USERS_EDIT` etc., should I change it ? – user3529850 Oct 03 '17 at 14:37
  • If you enable `sameOriginDisabled` you have to obtain a CSRF token from the server in order to init a connection. This is kind of a pain in the ass since most of the JS libraries don't send http headers along with the HTTP request anyway... If i were you i would just re-disable the CSRF. BUT be sure that you are aware of the risks, run some google search about CSRF if you are not familiar with. Also i believe that CSRF implies sessions, so you should most likely remove `.sessionCreationPolicy(SessionCreationPolicy.STATELESS)`. – Anthony Raymond Oct 03 '17 at 14:53
  • AbstractWebSocketMessageBrokerConfigurer has been deprecated. – Sõber Feb 11 '18 at 22:55
  • 1
    You can use [WebSocketMessageBrokerConfigurer](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurer.html) instead. – Anthony Raymond Feb 12 '18 at 09:19
  • I too needed this, I did as mentioned, but if CONNECT call throws exception, SUBSCRIBE & SEND are working fine. I thought if CONNECT throws exception, other frames won't work. Can you correct me if I am wrong?. I'm passing headers in all the frames and I'm authenticating now. But I feel this seems wrong. – Manoj Mar 22 '18 at 16:38
  • 1
    Yep, it definely seems wrong, i'll need to take a look at that. Unfortunately i don't have much times these days. I'll get back to you as soon i had enought time to check if out – Anthony Raymond Mar 22 '18 at 21:31
  • @Manoj can you describe how you succed in sending other frame with an error on CONNECT ? Or maybe a link to your client project code? – Anthony Raymond Apr 09 '18 at 22:39
  • 1
    sorry, I couldn't replicate it now. If I come across again, I will post the code. – Manoj Apr 11 '18 at 18:21
  • getAuthenticatedOrFail is failing because, after sending the request, the password change itself to "PROTECTED" and the pass comparasion fails. If I change the header name from "passcode" to "pass" (or something else) everything works thine. What am I doing wrong? – Hugo Sartori Jan 14 '19 at 21:37
  • i do'nt think tha tthe password is changinf from something to PROTECTED. On the other hand, most of the time spring's developper override the `.toString()` methods of objects and show PROTECTED instead of the password. I think that this is what you are talking about. If you add a breakpoint on the line `fetchUserFromDb(username, password)` what do you see as a password? – Anthony Raymond Jan 15 '19 at 09:51
  • Then I really misunderstand something because I think this documentation clearly say that the common security settings are shared amongst the http and the websocket accesses: https://docs.spring.io/autorepo/docs/spring-security/4.1.x/reference/html/websocket.html#websocket-authentication – newhouse Aug 16 '19 at 11:26
  • 4
    Yep `WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made`. That is true, Websockets (so do stomp) rely on a first HTTP negotiation call, and spring expects that the authentication will take place here. But there is not a single stomp JS libs that forwards the credentials during this negotiation call. because of that you need to authenticate after the negotioation. And thus use the websocket to authenticate. That's why i sais that the two chains are decoupled. – Anthony Raymond Aug 16 '19 at 12:33
  • Can you please share `fetchUserFromDb(username, password)` method code. Because I am saving my password in DB in hashes. I don't know how to match the password. Also in my application i want to notify other users if an user logout from app. But i found once the `HttpSession` expires i am not able to send messages. So how can i do it. Thanks. – Nazim Nov 20 '19 at 07:08
  • @Nazim For the first point it's already well explain all over the internet, i'll let you google about that. For the second point i'm sorry but i have no idea – Anthony Raymond Nov 20 '19 at 10:05
  • @AnthonyRaymond I am using Redis for session. I tried debugging the `StompHeaderAccessor` and i got `null` for both `username` and `password` header. I am getting `x-auth-token` in header.Now can you please tell me how to verify the user ? Thanks. – Nazim Nov 21 '19 at 08:12
  • I could not be more agree that it is like "spending two days trying to understand" – abhishek ringsia Feb 22 '21 at 09:06
  • 7
    Still to this day, your post is the ONLY thing that works. Spring boot documentation is still lacking clear instructions. Thanks a lot – Erick Audet Sep 06 '21 at 16:38
  • Hi, I don't wanna use Stomp i just used this as websocket handler: registry.addHandler(activeUsersWebSocketHandler, "/active-users") but if i use this endpoint in the permitAll it will ignore and filter anyway, any help please – Alex Aug 28 '23 at 09:58
4

for java client side use this tested example:

StompHeaders connectHeaders = new StompHeaders();
connectHeaders.add("login", "test1");
connectHeaders.add("passcode", "test");
stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler());
Jens
  • 67,715
  • 15
  • 98
  • 113
tbw
  • 71
  • 7
  • This will work with stomp.js too. But these are *STOMP* headers (on the CONNECT frame). The original issue is that stomp.js won't set *HTTP* websocket-handshake headers that'd be used by Spring Security to automatically authenticate. – almondandapricot Jan 25 '21 at 13:46
1

Going with spring authentication is a pain. You can do it in a simple way. Create a web Filter and read the Authorization token by yourself, then perform the authentication.

@Component
public class CustomAuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String authorization = request.getHeader("Authorization");
            if (/*Your condition here*/) {
                // logged
                filterChain.doFilter(servletRequest, servletResponse);
            } else {
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.getWriter().write("{\"message\": "\Bad login\"}");
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

Then in your configuration define the filter using the spring mechanism:

@Configuration
public class SomeConfig {
    @Bean
    public FilterRegistrationBean<CustomAuthenticationFilter> securityFilter(
            CustomAuthenticationFilter customAuthenticationFilter){
        FilterRegistrationBean<CustomAuthenticationFilter> registrationBean
                = new FilterRegistrationBean<>();

        registrationBean.setFilter(customAuthenticationFilter);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}
Borislav Markov
  • 1,495
  • 11
  • 12
  • I agree that spring security is a long way, but doing so you loose all the convenience brought by spring-security (being able to get the User from anywhere in the request context, test mocking, ...) – Anthony Raymond May 21 '21 at 15:19
  • 1
    There is no convenience in complexity. In my approach you still can inject your service and check username and password. For example you can inject WebSocketAuthenticatorService and do the check by splitting Authorization and get the username pass if this is Basic AUTH. There is no right or wrong solution here as long as it works, I prefer this as it is more elegant for me. Others might prefer other. – Borislav Markov May 22 '21 at 07:04
0

If you are using this Stomp Client library for Android then you actually can add Http headers to initial handshake request.

Here is an example:

Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Your auth token");
    
StompClient stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, 
        "ws://example.com/api/websocket", headers);
Eugene
  • 177
  • 2
  • 5