I’m using a custom WebSocketChannelInterceptor (implements ChannelInterceptor) to handle authentication during the CONNECT STOMP command. The interceptor extracts and validates a JWT token from the Authorization header like this:
@Override
public Message<?> preSend(@NonNull Message<?> message, @NonNull MessageChannel channel) {
var accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getCommand();
if (command == null) {
return null;
}
return switch (command) {
case CONNECT -> handleConnect(message, accessor);
case SEND -> handleSend(message, accessor);
default -> message;
};
}
private Message<?> handleConnect(Message<?> message, StompHeaderAccessor accessor) {
String authorizationHeader = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith(JwtService.BEARER_PREFIX)) {
throw new MessageHandlingException(message, "Missing or invalid authorization header");
}
String token = authorizationHeader.substring(JwtService.BEARER_PREFIX.length()).trim();
try {
var jwtAuthToken = new JwtAuthenticationToken(token);
authManager.authenticate(jwtAuthToken);
return message;
} catch (BadCredentialsException e) {
throw new MessageHandlingException(message, e.getMessage());
} catch (RuntimeException e) {
throw new MessageHandlingException(message, "Internal server error");
}
}
My question is: How can I access the authenticated user's Principal inside the SessionConnectedEvent handler?
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
@EventListener
public void sessionConnectedEvent(SessionConnectedEvent event) {
// How to get Principal here?
}
}
I’m not interested in SessionConnectEvent — I specifically want to get the user from SessionConnectedEvent after the handshake and connection are completed.
Thanks in advance!
I've tried many different approaches, but none of them worked for my case. The token is sent during the initial WebSocket connection using STOMP headers like this:
const client = new Client({
brokerURL: 'ws://localhost:8080/ws',
connectHeaders: {
"Authorization": `Bearer ${accessToken}`,
},
onStompError: await onStompError,
onConnect: () => {
console.log("Successfully connected");
},
debug: console.debug
});
The way I solved the authentication problem was that in my preSend
I set the authentication token from the headers that contain it:
public final class WebsocketAuthenticationInterceptor implements ChannelInterceptor {
public static final String SIMP_USER_HEADER = "simpUser";
@SneakyThrows
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
setAuthenticationIfPresent(message);
return message;
}
private void setAuthenticationIfPresent(Message<?> message) {
if (message instanceof GenericMessage<?> genericMessage &&
genericMessage.getHeaders().get(SIMP_USER_HEADER) instanceof PreAuthenticatedAuthenticationToken token) {
SecurityContextHolder.getContext().setAuthentication(token);
}
}
}
and afterwards you can always access it anywhere (including the sessionConnectedEvent
method) via SecurityContextHolder.getContext().getAuthentication()
Depending on where you are trying to access the Authenticated Principal, you might have to set SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
to allow child threads to access the Authentication Object that was set by the parent thread. For that just add this to any Configuration Bean:
@PostConstruct
void setGlobalSecurityContext() {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}