Spring WebSocket StompHeaderAccessor - java

If i execute following code, sha.getLogin() and sha.getPasscode() outputs null !?
What is wrong with the code?
Client:
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect("123","456", function (frame) {
//...
});
Server:
#EventListener
private void onSessionConnect(SessionConnectedEvent event)
{
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
System.out.println(sha.getLogin());
System.out.println(sha.getPasscode());
}
But if execute following command, the login and passcode is contained.
sha.getMessageHeaders().toString()
Output (no json):
{
simpMessageType=CONNECT_ACK,
simpConnectMessage=GenericMessage[
payload=byte[0],
headers={
simpMessageType=CONNECT,
stompCommand=CONNECT,
nativeHeaders={
login=[123],//<<<Login
passcode=[PROTECTED],//<<<Passcode
accept-version=[
1.1,
1.0
],
heart-beat=[
10000,
10000
]
},
simpSessionAttributes={},
simpHeartbeat=[J#4b5cea63,
stompCredentials=[PROTECTED],
simpSessionId=xhojby2n
}
],
simpSessionId=xhojby2n
}

You can use accessor.getPasscode() method instead of accessor.getFirstNativeHeader(PASSWORD_HEADER)
bacause default StompDecoder protect passcode when setting
public void setPasscode(#Nullable String passcode) {
setNativeHeader(STOMP_PASSCODE_HEADER, passcode);
protectPasscode();
}
private void protectPasscode() {
String value = getFirstNativeHeader(STOMP_PASSCODE_HEADER);
if (value != null && !"PROTECTED".equals(value)) {
setHeader(CREDENTIALS_HEADER, new StompPasscode(value));
setNativeHeader(STOMP_PASSCODE_HEADER, "PROTECTED");
}
}

Spring is accessing the session data when you call sha.getLogin() or sha.getPasscode() but your user isn't authenticated on session, when you send connect ACK you need to intercept the message and authenticate user on the session.
Take a look at Spring Security WebSocket Support & Sessions

Related

How to differentiate headers between two/multiple endpoints in a RequestInterceptor

Hello I'm new to Java and Springboot. I'm currently working with an API where before making a POST request, I would need to generate a Bearer token. In order to generate a Bearer token, I would need to pass in my basic auth credentials to the "/oauth/token" endpoint. My application is having trouble passing my basic auth credentials since by the time I hit the "/v1/some-endpoint", I'm denied authorization because the Bearer token is null.
Here's my initial solution thinking I could check the url in the interceptor, then executing the following line but after debugging, it doesn't seem to be hitting that line.
Is there something I'm missing or not implementing correctly? Am I not implementing the Basic Auth endpoint correctly? Let me know if you need more information. Thanks
#Profile("!offline")
#FeignClient(
value = "someClient",
url = "${someProperty.url}",
configuration = SomeClient.SomeClientConfig.class)
public interface someClient {
#PostMapping("/v1/some-endpoint")
void redeemSomething(someRequestBody data);
#PostMapping("/oauth/token")
static BasicAuthResponse getBasicAuthToken() {
return new BasicAuthResponse();
}
#AllArgsConstructor
class SomeClientConfig extends BaseClientConfig {
private final SomeProperties properties;
private final SomeAuthTokenSupplier tokenSupplier = new SomeAuthTokenSupplier();
#Bean
#Override
public CloseableHttpClient apacheClient() {
return apacheClientFactory(properties.getUseProxy());
}
#Bean
public RequestInterceptor someAuthInterceptor() {
return template -> {
if(template.url().equals("/oauth/token")) {
String authToken = Base64Utils.encodeToString((properties.getCredentials().getUser() + ":" + properties.getCredentials().getUser()).getBytes(Charset.forName("UTF-8")));
template.header("Authorization", authToken);
}
template.header("Authorization", String.format("Bearer %s", tokenSupplier.getToken()));
};
}
private class SomeAuthTokenSupplier {
private volatile String token;
private volatile long retrievedOn = -1L;
String getToken() {
if (updateTokenRequired()) {
synchronized (this) {
if (updateTokenRequired()) {
BasicAuthResponse tokenResponse = getBasicAuthToken();
token = tokenResponse.getAccess_token(); // new token from some api should be assigned here
retrievedOn = Instant.now().toEpochMilli();
}
}
}
return token;
}
private boolean updateTokenRequired() {
return token == null || LocalDateTime.now().minusHours(8L).isAfter(LocalDateTime.ofInstant(Instant.ofEpochMilli(retrievedOn), ZoneId.systemDefault()));
}
}
#Override
public Retryer retryer() {
return new ClientRetry(250L, 2, 3) {
#Override
public void continueOrPropagate(RetryableException e) {
if (e.status() == 401 || e.status() == 403) {
tokenSupplier.token = null;
}
super.continueOrPropagate(e);
}
};
}
}
}
It worth using standard Spring Security OAuth2 Client feature instead in order to support authorization in Feign clients
See docs and code samples: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2client
UPD
See another code sample: https://github.com/int128/feign-oauth2-example
If several service endpoints require different authentication, then it's worth having several Feign clients, each with own configuration

#Scheduled not working with Web Sockets & Destination Variable

When a user goes to a specific end-point to find a stock-price, I want the stock price to update on the page every 3 seconds.
#Scheduled is not working on the following code:
#MessageMapping("/stocks/{name}")
#Scheduled(fixedRate = 3000)
public void stockData(#DestinationVariable String name) throws Exception {
Stock stock = YahooFinance.get(name);
simpMessagingTemplate.convertAndSend("/topic/stocks/" + name, stock);
}
JS front-end code:
var stompClient = null;
function connect(url) {
console.log("connect() called")
var socket = new SockJS('/stocks');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log("connected")
setConnected(true);
stompClient.subscribe('/topic/stocks/' + url, function(
retrieveSingleStockData) {
console.log('retrieving...');
display(retrieveSingleStockData.body);
});
}, function(err) {
console.log(err);
});
}
The stomp-client can subscribe without any issues, but the method in the controller isn't sending back any data. I know it's an issue with the #Scheduled annotation.
I'd appreciate any help, thanks

How to get Authentication from X-Auth-Token?

My goal is to authenticate the WebSocket CONNECT frame. I wish to be able to initialize Authentication user = ... by using X-Auth-Token.
TL:DR
I use the X-Auth-Token header. How the current authentication works:
User hit POST /login endpoint with Form Data username and password.
The response header will contain the key X-Auth-Token.
If you hit any REST endpoint with X-Auth-Token the server will recognize the user.
The issue is how to get Authentication from X-Auth-Token in the WebSocket CONNECT frame.
The current solution is to use JWT, however, one of the requirements for this project is a user should be able to invalidate the session. For JWT to be able to do that, the JWT should be a stateful, reference to this SO's the question
#Configuration
#EnableWebSocketMessageBroker
// see: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-authentication-token-based
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String sessionId = accessor.getFirstNativeHeader("X-AUTH-TOKEN");
// Authentication user; // How to get Authentication from X-Auth-Token?
// accessor.setUser(user);
}
return message;
}
});
}
}
What I did:
I change Cookie-based authentication by letting the session be provided in a header.
// see: https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-rest
#Configuration
// Override HttpSession's Filter, in this instance Spring Session is backed by Redis.
#EnableRedisHttpSession
public class HttpSessionConfig {
// Default connection configuration, to localhost:6739.
#Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
// Tell Spring to use HTTP headers, X-Auth-Token.
#Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
}
logic to CONNECT and SUBSCRIBE
const X-Auth-Token = "" // get from the POST `/login` endpoint
const onConnectCallback = () => {
const destinations = ["/topic/channel/1", "/user/queue/messages"];
for (let i = 0; i < destinations.length; i++) {
stompClient.subscribe(destinations[i], (payload) => {
// receiveMessageCallback
});
}
};
const stompConfig = {
brokerURL: "ws://localhost:8080/chat",
connectHeaders: {
"X-Auth-Token": X_Auth_Token,
},
onConnect: onConnectCallback,
};
const stompClient = new StompJs.Client(stompConfig);
stompClient.activate();
Reference
I am worried that X-Auth-Token is not supported in WebSocket because based on SO's answer there is no API to retrieve session by id

Problem with authenticating private channels in laravel with java client

I want to send broadcast messages from server (using laravel) to clients (using java).
What I'm using
Pusher as boradcast driver.
laravel passport for api authentication.
What I've done in server side
I've configured my Pusher credentials in .env file.
Uncommented App\Providers\BroadcastServiceProvider::class line in config/app.php file.
In config/auth.php file I've added the following:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'devices' => [
'driver' => 'session',
'provider' => 'devices',
],
'api' => [
'driver' => 'passport',
'provider' => 'devices',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// using devices table to authenticate over api guard
'devices' => [
'driver' => 'eloquent',
'model' => App\Device::class,
],
],
In App\Providers\BroadcastServiceProvider class I added the following to boot() function:
Broadcast::routes(['prefix' => 'api', 'middleware' => 'auth:api']);
In routes/channels.php I added the following:
Broadcast::channel('device.{device_id}', function ($device, $device_id) {
return $device->id === $device_id;
});
Created an event AdvertisementAdded by running php artisan make:event AdvertisementAdded, added implements ShouldBroadcast then added the following to its broadcastOn() method:
return new PrivateChannel('device.'.$this->device_id);
What I've done in client side
Because I'm just testing now, I got my access_token and device_id by sending a login request from postman
I copied that accessToken to my java client and stored it in accessToken variable as String, here's the code:
String accessToken = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImY3ZTVlMTAzZWE3MzJjMTI5NzY1YTliMmMzOTM0N2ZhOGE4OTU5MjRjNDA5ZjgyOTA4ZDg5NTFjZTBkOGZlNTA2M2M1YTI1MDBlOTdhZDdiIn0.eyJhdWQiOiIxIiwianRpIjoiZjdlNWUxMDNlYTczMmMxMjk3NjVhOWIyYzM5MzQ3ZmE4YTg5NTkyNGM0MDlmODI5MDhkODk1MWNlMGQ4ZmU1MDYzYzVhMjUwMGU5N2FkN2IiLCJpYXQiOjE1NTkwOTYyNDgsIm5iZiI6MTU1OTA5NjI0OCwiZXhwIjoxNTkwNzE4NjQ3LCJzdWIiOiI3Iiwic2NvcGVzIjpbXX0.FKeE9Z-wv2yUNQPl-qsbu9baYGTdbQ6DuzaI1R8azR6l1CIP9uRI4hCaoWvgx0GXWWLPRNhfQl-YD3KP2YOraW16-h4ie_95B9VQrpFxXnlqKojsfh1xSrSNSl5HncslMWQPVjoesBpM5y_cpG19PGgu-SWo0W6s9Fiz_Nm70oyyZB9mSqU8PVQvAOSNr6TMR0aC3iMLFfkyZkTSwj8EoRyD2LGW6v4PFriqx8JLbZASCOiUYBlYnunWrTFDOAenZcoa5Sw7u7kbSvYehjDKRwKjQM6zmPfi0A3Mp0CHjHE599OXb-NG2IMH-wmlT0vEZjP2U97hxmsNW1RtHNXWaRKFL9T-WVmZbJf3fH5hXqTv495L3MQfq_m5YFHyc5NuIqK4K4xMJB956a33ICnH8DmvPmJgderNAhqEX1JHUAsR63K7xbZxRBDS8OlQYcEf-_v75X0kT1s067enSvI8Vs212AVnI6k0FmgQNM8DfJUq6YduD0m2F2ZWpKPrwdd6PdW5ZlZTEv-D8dYIEQ_CwOWohNoENATmTqxDpPBxK5c723MEt8S7Sa9MEGAo56HW3-9pbazbEdY1GqPWKVkov7K_6eBFcWsV67AgJpoKFt6RiBfRvokgiH96WG89qBB_Ucpm8uBahX93FaOXhVLW0VjJH2LQKrGw0bb5LS8Ql5o";
String deviceId = "7";
Map<String, String> authHeaders = new HashMap();
authHeaders.put("Authorization", accessToken);
HttpAuthorizer authorizer = new HttpAuthorizer("http://localhost:8000/api/broadcasting/auth");
authorizer.setHeaders(authHeaders);
PusherOptions options = new PusherOptions();
options.setAuthorizer(authorizer).setCluster(PUSHER_CLUSTER);
Pusher pusher = new Pusher(PUSHER_APP_KEY, options);
pusher.subscribePrivate("private-device." + deviceId, new PrivateChannelEventListener() {
#Override
public void onEvent(String channelName, String eventName, final String data) {
System.out.println(String.format("Received event on channel [%s]", channelName));
}
#Override
public void onSubscriptionSucceeded(String string) {
System.out.println(String.format("Subscribed to channel [%s]", string));
}
#Override
public void onAuthenticationFailure(String string, Exception excptn) {
System.out.println(string);
}
});
pusher.connect(new ConnectionEventListener() {
#Override
public void onConnectionStateChange(ConnectionStateChange change) {
System.out.println("State changed to " + change.getCurrentState() +
" from " + change.getPreviousState());
}
#Override
public void onError(String message, String code, Exception e) {
System.out.println("There was a problem connecting!");
}
});
// Keeping main thread alive
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
When running the code above, it outputs the following on console:
State changed to CONNECTING from DISCONNECTED
State changed to CONNECTED from CONNECTING
java.io.IOException: Server returned HTTP response code: 403 for URL: http://localhost:8000/api/broadcasting/auth
I'm sure that auth:api middleware is working as I expect on other requests.
Here's a snippet from my routes/api.php:
Route::middleware('auth:api')->group(function () {
Route::prefix('advertisements')->group(function () {
Route::get('/request', 'AdvertisementsController#getDeviceAdvertisements')
->name('advertisements.getDeviceAdvertisements');
});
});
And here's a test to that route from postman (with the same access token as above):
And here's a test to api/broadcasting/auth route from postman (with the same access token as above):
What's the problem? Why all api routes under auth:api middleware working properly but not api/broadcasting/auth route??
Note
I tried working with public channels with no problems.
After a whole day of searching, finally It's solved.
The error happens when authorizing the channel, not when authenticating the request using auth:api middleware.
My private channel authorizing function in routes/channels.php always returns false meaning it will reject all subscribing requests to private-device.{device_id} channel:
Broadcast::channel('device.{device_id}', function ($device, $device_id) {
// this always return false, because of inequality of types
return $device->id === $device_id;
});
Authorizing function above always return false, because of inequality of types between $device->id (which is of type int) and $device_id (which is of type string).
So, in order to solve the problem, I cast both of them to int and then checked for equality.
Here's the code I used to solve the problem:
Broadcast::channel('device.{device_id}', function ($device, $device_id) {
return (int) $device->id === (int) $device_id;
});

Spring+WebSocket+STOMP. Message to specific session (NOT user)

I am trying to set up basic message broker on Spring framework, using a recipe I found here
Author claims it has worked well, but I am unable to receive messages on client, though no visible errors were found.
Goal:
What I am trying to do is basically the same - a client connects to server and requests some async operation. After operation completes the client should receive an event. Important note: client is not authenticated by Spring, but an event from async back-end part of the message broker contains his login, so I assumed it would be enough to store concurrent map of Login-SessionId pairs for sending messages directly to particular session.
Client code:
//app.js
var stompClient = null;
var subscription = '/user/queue/response';
//invoked after I hit "connect" button
function connect() {
//reading from input text form
var agentId = $("#agentId").val();
var socket = new SockJS('localhost:5555/cti');
stompClient = Stomp.over(socket);
stompClient.connect({'Login':agentId}, function (frame) {
setConnected(true);
console.log('Connected to subscription');
stompClient.subscribe(subscription, function (response) {
console.log(response);
});
});
}
//invoked after I hit "send" button
function send() {
var cmd_str = $("#cmd").val();
var cmd = {
'command':cmd_str
};
console.log("sending message...");
stompClient.send("/app/request", {}, JSON.stringify(cmd));
console.log("message sent");
}
Here is my configuration.
//message broker configuration
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
config.enableSimpleBroker("/topic");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/cti")
.setAllowedOrigins("*")
.withSockJS();
}
}
Now, after basic config I should implement an application event handler to provide session-related information on client connect.
//application listener
#Service
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
//this is basically a concurrent map for storing pairs "sessionId - login"
WebAgentSessionRegistry webAgentSessionRegistry;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String agentId = sha.getNativeHeader("Login").get(0);
String sessionId = sha.getSessionId();
/** add new session to registry */
webAgentSessionRegistry.addSession(agentId,sessionId);
//debug: show connected to stdout
webAgentSessionRegistry.show();
}
}
All good so far. After I run my spring webapp in IDE and connected my "clients" from two browser tabs I got this in IDE console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Okay, now let's try to implement message mechanics.
//STOMPController
#Controller
public class STOMPController {
#Autowired
//our registry we have already set up earlier
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
//a helper service which I will post below
MessageSender sender;
#MessageMapping("/request")
public void handleRequestMessage() throws InterruptedException {
Map<String,String> params = new HashMap(1);
params.put("test","test");
//a custom object for event, not really relevant
EventMessage msg = new EventMessage("TEST",params);
//send to user2 (just for the sake of it)
String s_id = webAgentSessionRegistry.getSessionId("user2");
System.out.println("Sending message to user2. Target session: "+s_id);
sender.sendEventToClient(msg,s_id);
System.out.println("Message sent");
}
}
A service to send messages from any part of the application:
//MessageSender
#Service
public class MessageSender implements IMessageSender{
#Autowired
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
SimpMessageSendingOperations messageTemplate;
private String qName = "/queue/response";
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
#Override
public void sendEventToClient(EventMessage event,String sessionId) {
messageTemplate.convertAndSendToUser(sessionId,qName,event,createHeaders(sessionId));
}
}
Now, let's try to test it. I run my IDE, opened Chrome and created 2 tabs form which I connected to server. User1 and User2. Result console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Sending message to user2. Target session: 10g5e10n
Message sent
But, as I mentioned in the beginning - user2 got absolutely nothing, though he is connected and subscribed to "/user/queue/response". No errors either.
A question is, where exactly I am missing the point? I have read many articles on the subject, but to no avail.
SPR-11309 says it's possible and should work. Maybe, id-s aren't actual session id-s?
And well maybe someone knows how to monitor if the message actually has been sent, not dropped by internal Spring mechanics?
SOLUTION UPDATE:
A misconfigured bit:
//WebSocketConfig.java:
....
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
// + parameter "/queue"
config.enableSimpleBroker("/topic","/queue");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
....
I've spent a day debugging internal spring mechanics to find out where exactly it goes wrong:
//AbstractBrokerMessageHandler.java:
....
protected boolean checkDestinationPrefix(String destination) {
if ((destination == null) || CollectionUtils.isEmpty(this.destinationPrefixes)) {
return true;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
//guess what? this.destinationPrefixes contains only "/topic". Surprise, surprise
return true;
}
}
return false;
}
....
Although I have to admit I still think the documentation mentioned that user personal queues aren't to be configured explicitly cause they "already there". Maybe I just got it wrong.
Overall it looks good, but could you change from
config.enableSimpleBroker("/topic");
to
config.enableSimpleBroker("/queue");
... and see if this works? Hope this help.

Categories

Resources