Is this a safe and good way to use Websockets for push notifications?

Hello

I have a platform where multiple restaurants (let's say 100) can receive online orders from users. The admin of the restaurant has a live panel where all the incoming orders are displayed. So when a new order is placed the admin must receive a push notification without reloading the page.

To accomplish this I have used Websockets. I have implemented this Websocket Chat Example.

So there are different chat rooms in which I can push a message from the server. I was thinking about the following way to implement the push notifications:

  1. When a restaurant admin opens his live order panel, a unique secret key gets generated server-side and saved in the client-side session of the admin.
  2. That secret key is the name of the chat-room.
  3. Each time an order gets placed a string with the ID of the order is sent through the websocket from the server to the admin's order-panel.
  4. AJAX retrieves the order details by using the ID received by the websocket.

Is this a good and safe implementation?

If messages only go from server to client, server-sent events may be an option. They are simpler than web sockets.

4 Likes

Yeah but I have already implemented the Websockets... So I am asking if my solution is okay as well. Are there safety or performance concerns by using websocket in the way I did?

Seems fine but a bit over-engineered maybe? The WebSocket is already within the context of the admin's login session, so what's the extra secret key gaining you?

3 Likes

I don't understand that part. Can't WebSocket be used for the entire transaction?

Re "safe" what are you concerned about?

The secret key is the name of the 'chat room'. There are different panels for each restaurant. If I'd do this:

// Javascript
socket.addEventListener('open', (event) => {
    socket.send('/name restaurant');
    socket.send('/join main');
});

All panels/restaurants would receive each others orders.

So for restaurant A I do:

// Javascript
socket.addEventListener('open', (event) => {
    socket.send('/name restaurant_a');
    socket.send('/join secret_key_restaurant_a');
});

And for restaurant B I do:

// Javascript
socket.addEventListener('open', (event) => {
    socket.send('/name restaurant_b');
    socket.send('/join secret_key_restaurant_b');
});

Yeah. I could indeed do the whole transaction from the websocket. Thanks for the input.

And I want to be sure that Restaurant A can't somehow receive/view the incoming orders of Restaurant B.

Why not use the unique identifier you use for the restaurant as key for your order channel? Seems easier than having another variable around.

socket.addEventListener('open', (event) => {
    socket.send('/name restaurant_a');
    socket.send('/join restaurant_a');
});

Your "secret" key does not sound like it is doing anything secret/important for access/authorization management, so it is just an identifier, right?

3 Likes

I currently don't have a unique identifier for a restaurant. Only a primary key ID (auto_increment) which of course is easy to guess.

Would you suggest adding a column secret_identifier to the database table containing the restaurants? Wouldn't a newly generated key for each session be more safe then?

No, I'd suggest that you keep access to the websocket order stream for each restaurant secure not with a secret, but through user authentication.

I don't know how you manage authentication and authorization in your app, but lets assume you are using access tokens (i.e. OpenID Connect) for now (if you use sessions or some other mechanism, I'm sure this example is translatable).

Say your admin logs into your application and navigates to the orders page. The order page establishes a connection to the websocket stream by calling GET /restaurant/{restaurant 1}/orders. Now in order to make sure the authenticated user can access the websocket stream for restaurant 1, I'd send the access token (or session id, or other form of credentials) as part of the HTTP request's header, for example the authorization header as bearer token: Authorization: Bearer $TOKEN. The endpoint validates the credentials and makes sure the user has access based on the rules you define (i.e. user belongs to group restaurant 1 and has admin rights in that group). If that operation is successful, you return the successfully established websocket connection. If not, return a error 401 Unauthorized.

2 Likes

Sorry. This is a new concept for me to grasp. Thanks for all the help.

Is this getting close?

async fn chat_route(
    req: HttpRequest,
    stream: web::Payload,
    srv: web::Data<actix::Addr<websocket_server::ChatServer>>,
    session: Session,
) -> Result<HttpResponse> {
    let mut restaurantadmin = match session.get::<RestaurantAdmin>("restaurantadmin").unwrap() {
        Some(admin) => admin,
        None => {
            return Ok(HttpResponse::Unauthorized().finish());
        }
    };

    ws::start(
        websocket_session::WsChatSession {
            id: 0,
            hb: std::time::Instant::now(),
            room: "main".to_owned(),
            name: None,
            addr: srv.get_ref().clone(),
        },
        &req,
        stream,
    )
}

Or would it be better to get the session-key client-side and pass that as a header with the websocket in Javascript?

1 Like

That looks like you check user authorization and only after that pans out you connect your websocket, which is what I had in mind. Where is that secret key coming into place? Or have you removed it already from the example?

2 Likes

Okay. But then take a look at this piece of code which is after the authorization:

ws::start(
        websocket_session::WsChatSession {
            id: 0,
            hb: std::time::Instant::now(),
            room: "main".to_owned(),
            name: None,
            addr: srv.get_ref().clone(),
        },
        &req,
        stream,
    )

I have this in my Javascript:

socket.addEventListener('open', (event) => {
    socket.send('/name abc');
    socket.send('/join main');
});

All restaurants have the same Javascript code of course. So all get subscribed to room 'main'. This means all orders will get pushed to the chatroom 'main' and thus all restaurants will receive ALL the orders and not only the orders unique to them.

If I'd only have one restaurant. This simple authorization would suffice. But there will be multiple restaurants with multiple live panels.

I push the incoming orders like this:

async fn send_order_test(
    srv: web::Data<actix::Addr<websocket_server::ChatServer>>,
) -> Result<HttpResponse> {
    srv.send(websocket_server::ClientMessage {
        id: 0,
        msg: "New order incoming with ID 1234".to_string(),
        room: "main".to_string(),
    })
    .await;

    Ok(HttpResponse::Ok().content_type("text/plain").body("OK"))
}

Right now all the orders get pushed in the same room. Each restaurant should have it's own room, no?

1 Like

Exactly. If the user doesn't have access to that room, he can't connect to that room.

You can probably use the session object with the user information for connecting to the right room (if your users can only ever belong to one restaurant) or send something as part of the connection request.

2 Likes

Like this?

async fn chat_route(
    req: HttpRequest,
    stream: web::Payload,
    srv: web::Data<actix::Addr<websocket_server::ChatServer>>,
    session: Session,
) -> Result<HttpResponse> {
    let mut restaurantadmin = match session.get::<User>("restaurantadmin").unwrap() {
        Some(admin) => admin,
        None => {
            return Ok(HttpResponse::Unauthorized().finish());
        }
    };

    let restaurant = restaurantadmin.restaurant_name;

    ws::start(
        websocket_session::WsChatSession {
            id: 0,
            hb: std::time::Instant::now(),
            room: restaurant.to_string().to_owned(),
            name: None,
            addr: srv.get_ref().clone(),
        },
        &req,
        stream,
    )
}
$(document).ready(function() {
const socket = new WebSocket('ws://127.0.0.1:8080/ws');


// Connection opened
socket.addEventListener('open', (event) => {
    socket.send('/name abc');
    socket.send('/join my_restaurant_name');
});

I mean I don't understand the messages you send on that channel when you connect, especially /join my_restaurant_name looks like it isn't doing anything, because we already established which room the client belongs to in this call:

but other than that, this is pretty much how I'd do it.

3 Likes

You're right! I understand it. Thanks mate. You're awesome.

2 Likes

Each WebSocket connection has a unique identifier by default, correct?