I have a Spring Boot application that uses Spring Security to secure endpoints using JWT, which all works correctly for standard REST controllers. The app also has WebSocket connections, which I have successfully secured using the same JWT.
The Problem
If I try to inject the Principal into the WebSocket controller method, the Principal is null by default. I have found many solutions of setting the Principal by overriding the DefaultHandshakeHandler, however most of these solutions generate a random Principal name. I would rather use the existing logged in username to generate the Principal name so that I can see which user is sending data to the WebSocket and handle the response accordingly.
There is a reply here that I thought would get me closer by adding , but when applying the logic to my preSend method I get a null username even once I've added it as a parameter of my StompJS connection method: WebSocket Stomp over SockJS - http custom headers
What I also find strange is within the interceptor itself I am able to access the Principal with the correct username, but once it reaches the controller it is either null or whatever was defined in the preSend handshake method.
Here is the method used to connect to the WebSocket:
_connect() {
console.log("Initialize WebSocket Connection");
if (this.authenticationService.isUserLoggedIn()) {
let ws = new SockJS(this.webSocketEndPoint);
this.stompClient = Stomp.over(ws);
const _this = this;
_this.stompClient.connect({
'Authorization': "Bearer " + this.authenticationService.getLoggedInUserToken(),
'username': this.authenticationService.getLoggedInUserName()
}, function (frame) {
_this.stompClient.subscribe(_this.board, function (data) {
_this.onDataReceived(data);
});
}, this.errorCallBack);
} else {
console.log("Unable to connect: must be logged in.");
}
}
And this is my current handshake code with commented results:
public class MyHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
Principal principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL HANDSHAKE START:" + principal); //prints annonymous user
final String ATTR_PRINCIPAL = "__principal__";
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
String username = httpServletRequest.getParameter("username");
//System.out.println("Username is: " + username); // null
final String name;
if (!attributes.containsKey(ATTR_PRINCIPAL)) {
name = "My made up name"; //Would like this to be the logged in user, but is currently null
attributes.put(ATTR_PRINCIPAL, name);
} else {
name = (String) attributes.get(ATTR_PRINCIPAL);
}
return new Principal() {
@Override
public String getName() {
return name;
}
};
}
}
And finally my interceptor that authorises WebSocket requests and shows the correct logged in user:
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
Principal principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL PRE-SEND START:" + principal); // shows annonymous user
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List<String> tokenList = accessor.getNativeHeader("Authorization");
String jwt = null;
if (tokenList == null || tokenList.size() < 1) {
return message;
} else {
jwt = tokenList.get(0).substring(7);
if (jwt == null) {
return message;
}
}
String username = jwtUtil.extractUsername(jwt); //Decoder class that can retrieve username from token
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Used only to see which principal is found at this stage
principal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL PRE-SEND END:" + principal); // shows correct logged in user
accessor.setUser(authentication);
}
return message;
}
});
}
}
And here's the Controller method currently being tested for the Principal:
@MessageMapping("/test")
public void test(@Payload String message, Principal principal) throws Exception {
System.out.println("PRINCIPAL TEST INJECTED: " + principal.getName());
Principal retrievedPrincipal = SecurityContextHolder.getContext().getAuthentication();
System.out.println("PRINCIPAL TEST RETRIEVED: " + retrievedPrincipal); //null
webSocket.convertAndSend("/topic/test", new ResponseMessage("TestSuccess", "Thanks for the request " + principal.getName())); // shows "made up username" from handshake
}
Here is the console output when a logged in user called "example" connects to the WebSocket and tries to access the /app/test endpoint:
PRINCIPAL HANDSHAKE START:AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND END:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=example, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
PRINCIPAL PRE-SEND START:null
PRINCIPAL PRE-SEND START:null
PRINCIPAL TEST INJECTED: My made up name
PRINCIPAL TEST RETRIEVED: SecurityContextImpl [Null authentication]
Please let me know if you need any further code for clarity.