javaspringspring-securitycometcometd

Cometd, Spring-security : Currently authenticated user not available inside a Listener


I am working on a Spring-MVC project where I am using Spring-security for authentication and authorization. Along with that, I am using Cometd library for sending messages over websockets. After receiving the message inside a Listener, when I try to get the currently authenticated user, it's always null.

How can I make sure that each request in Cometd atleast contains the JSESSIONID which is required by Spring-security for identifying?

Or is there some setting in Spring-security which can make this possible.

As I looked up, there are many users facing this issue, but no definitive answer or code which is helpful.

Test code :

  @Listener(value = "/service/testlistener/{id}")
    public void testlistener(ServerSession remote,
                             ServerMessage message, @Param("id") String id) {
        try {            
          Person user = this.personService.getCurrentlyAuthenticatedUser();
            Map<String, Object> data = message.getDataAsMap();
           System.out.println("currentlyAuthenticatedUser: " + user + " \nTransmitted Data in map:" + data.get("name"));
            Map<String, Object> output = new HashMap<>();
            output.put("name", user.getFirstName());
            ServerChannel serverChannel = bayeux.createChannelIfAbsent("/person/" + id).getReference();
            serverChannel.setPersistent(false);
            serverChannel.publish(serverSession, output);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

cometd.js :

 var connectionIntervall = null;
    var onlineElement = document.getElementById("browser-online");
    var cometd = $.cometd;
    cometd.configure({
        url: navigationController.generateCometDUrl(),
        logLevel: 'error',
        stickyReconnect: false,
        appendMessageTypeToURL: false,
        requestHeaders : navigationController.generateCometDHeaders()
    });

    var connects = 0;

    cometd.addListener('/meta/handshake', _metaHandshake);
    cometd.addListener('/meta/connect', _metaConnect);
    cometd.websocketEnabled = true;

    cometd.handshake();

Solution

  • Solution for: - CometD 4.0.2 - Spring 4.3.9.RELEASE - Spring Security 4.1.0.RELEASE

    In my project we faced similar problem while switching from "long pooling" to "websocket" connection. For clarification - one difference - we were using "user context" only during "handshake".

    With usage of "long pooling" Cookie with JSESSIONID was interpreted correctly by Spring and we were working in "user context". After switching to "websocket" it looked like "user context" was lost just before running message receiving code.

    We could not find proper solution - following code is in my opinion just a hack. But it worked for us.

    import org.cometd.bayeux.server.BayeuxServer;
    import org.cometd.bayeux.server.ServerMessage;
    import org.cometd.bayeux.server.ServerSession;
    import org.cometd.server.DefaultSecurityPolicy;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    
    public class BayeuxAuthenticator extends DefaultSecurityPolicy implements ServerSession.RemoveListener {
    
        // (...)
    
        @Override
        public boolean canHandshake( BayeuxServer server, ServerSession session, ServerMessage message ) {
            SecurityContextHolder.getContext().setAuthentication( ( UsernamePasswordAuthenticationToken ) message.getBayeuxContext().getUserPrincipal() );
            String user = securityService.getUserName();
            // (...)
    
            return true;
        }
    
        // (...)
    }
    

    Where following line is the most important

    SecurityContextHolder.getContext().setAuthentication( ( UsernamePasswordAuthenticationToken ) message.getBayeuxContext().getUserPrincipal() );
    

    Edit 2:

    We are receiving data from

    String user = securityService.getUserName();
    

    where securityService is Spring @Service protected with Spring @Secured(/* expected roles */). (@Secured annotation was producing us Authorization exceptions before setting context)

    Inside there is context read, and user data:

    SecurityContextHolder.getContext().getUserPrincipal().getName()
    

    But to be honest if you just need "user principals" it may be sufficient to read it from:

    message.getBayeuxContext().getUserPrincipal() // instanceof java.security.Principal
    

    Which in our case was of type

    org.springframework.security.authentication.UsernamePasswordAuthenticationToken
    

    Maybe in your case it is different subclass?

    For clarification - during web socket connection start we had passed Cookie with proper JSESSIONID in the request.

    WebSocket connection request:

    Accept-Encoding: gzip, deflate, br
    Accept-Language: en-US,en;q=0.9,pl;q=0.8
    Cache-Control: no-cache
    Connection: Upgrade
    Cookie: XSRF-TOKEN=<some token>; Idea-<some token>; lastOpenTab=sourcing/content; io=<some token>; BAYEUX_BROWSER=<some token>; JSESSIONID=<some token>
    Host: localhost:8090
    Origin: http://localhost:8090
    Pragma: no-cache
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: NTtWtXAL8ReIB0LjvI2G0g==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 ...