Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring-websockets : Spring security authorization not working inside websockets

I am working on a Spring-MVC application in which we have Spring-security for authentication and authorization. We are working on migrating to Spring websockets, but we are having an issue with getting the authenticated user inside a websocket connection. The security context simply doesn't exist in the websocket connection, but works fine with regular HTTP. What are we doing wrong?

WebsocketConfig :

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app").withSockJS();
    }
}

In the controller below, we are trying to get the currently authenticated user and it's always null

@Controller
public class OnlineStatusController extends MasterController{

    @MessageMapping("/onlinestatus")
    public void onlineStatus(String status) {
        Person user = this.personService.getCurrentlyAuthenticatedUser();
        if(user!=null){
            this.chatService.setOnlineStatus(status, user.getId());
        }
    }
}

security-applicationContext.xml :

  <security:http pattern="/resources/**" security="none"/>
    <security:http pattern="/org/**" security="none"/>
    <security:http pattern="/jquery/**" security="none"/>
    <security:http create-session="ifRequired" use-expressions="true" auto-config="false" disable-url-rewriting="true">
        <security:form-login login-page="/login" username-parameter="j_username" password-parameter="j_password"
                             login-processing-url="/j_spring_security_check" default-target-url="/canvaslisting"
                             always-use-default-target="false" authentication-failure-url="/login?error=auth"/>
        <security:remember-me key="_spring_security_remember_me" user-service-ref="userDetailsService"
                              token-validity-seconds="1209600" data-source-ref="dataSource"/>
        <security:logout delete-cookies="JSESSIONID" invalidate-session="true" logout-url="/j_spring_security_logout"/>
        <security:csrf disabled="true"/>
        <security:intercept-url pattern="/cometd/**" access="permitAll" />
        <security:intercept-url pattern="/app/**" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!--        <security:intercept-url pattern="/**" requires-channel="https"/>-->
        <security:port-mappings>
            <security:port-mapping http="80" https="443"/>
        </security:port-mappings>
        <security:logout logout-url="/logout" logout-success-url="/" success-handler-ref="myLogoutHandler"/>
        <security:session-management session-fixation-protection="newSession">
            <security:concurrency-control session-registry-ref="sessionReg" max-sessions="5" expired-url="/login"/>
        </security:session-management>
    </security:http>
like image 467
We are Borg Avatar asked Sep 07 '25 09:09

We are Borg


1 Answers

I remember stumbling across the very same problem in a project I was working on. As I could not figure out the solution using the Spring documentation - and other answers on Stack Overflow were not working for me - I ended up creating a workaround.

The trick is essentially to force the application to authenticate the user on a WebSocket connection request. To do that, you need a class which intercepts such events and then once you have control of that, you can call your authentication logic.

Create a class which implements Spring's ChannelInterceptorAdapter. Inside this class, you can inject any beans you need to perform the actual authentication. My example uses basic auth:

@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {

    @Autowired
    private DaoAuthenticationProvider userAuthenticationProvider;

    @Override
    public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {

        final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        StompCommand cmd = accessor.getCommand();

        if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
            Authentication authenticatedUser = null;
            String authorization = accessor.getFirstNativeHeader("Authorization:");
            String credentialsToDecode = authorization.split("\\s")[1];
            String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
            String[] credentialsDecodedSplit = credentialsDecoded.split(":");
            final String username = credentialsDecodedSplit[0];
            final String password = credentialsDecodedSplit[1];
            authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            if (authenticatedUser == null) {
                throw new AccessDeniedException();
            } 
            SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
            accessor.setUser(authenticatedUser);    
        }
        return message;
    }
}

Then, in your WebSocketConfig class, you need to register your interceptor. Add the above class as a bean and register it. After these changes, your class would look like this:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
    

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app").withSockJS();
    }
    
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(authInterceptorAdapter);
        super.configureClientInboundChannel(registration);
    }
}

Obviously, the details of the authentication logic are up to you. You can call a JWT service or whatever you are using.

like image 120
AlgorithmFromHell Avatar answered Sep 09 '25 05:09

AlgorithmFromHell