Azure / azure-webpubsub

Azure Web PubSub Service helps you to manage WebSocket connections and do publish and subscribe in an easy way
https://azure.github.io/azure-webpubsub/
MIT License
132 stars 84 forks source link

Client-side only #179

Open eirikb opened 3 years ago

eirikb commented 3 years ago

Hi

Is it possible to use only a client (browser)?
All the examples I've seen so far either rely on creating a temporary token in portal, and/or use some middleware e.g., in nodejs.

I might be misunderstanding something, but

eirikb commented 3 years ago

I haven't been able to find a way to disable anything in the service, but it is quite straight forward to generate a token client-side.
The package @azure/web-pubsub does not seem to work in a browser (at least not when I tried with parcel, too many node specific dependencies), but the code for generating the token is simple, and does not require any external queries.

To play around I copied this code from the npm package, and used the secret to build a token.

This means super secret key and connection string will be public.
You might think this is madness, and it probably is, I'm just not sure why yet - can the service be managed with the key?


Update: Code sample

If anyone is interested, here is the code I made:

import jwt from "jsonwebtoken";

export function tokenUrl(host: string, key: string, hub: string): string {
  const clientUrl = `wss://${host}/client/hubs/${hub}`;
  const audience = `https://${host}/client/hubs/${hub}`;
  const signOptions: jwt.SignOptions = {
    audience: audience,
    expiresIn: "60m",
    algorithm: "HS256",
  };
  const payload = {
    role: ["webpubsub.sendToGroup", "webpubsub.joinLeaveGroup"],
  };
  const token = jwt.sign(payload, key, signOptions);
  return `${clientUrl}?access_token=${token}`;
}

Notes about the code:

vicancy commented 3 years ago

please don't do this for a publicly accessible web page, doing this means everyone who can view your web page can abuse your service with this connection string.

eirikb commented 3 years ago

@vicancy Thank you for your answer. Could you elaborate on that please? What are the full extent of implications when making the connection string public?

vicancy commented 3 years ago

In general, when you have the connection string you can use all the features the service provided, for example, connect to the service, call the REST API of the service to broadcast to any connected clients, close any clients, etc. In a word, when you have the connection string, you are the admin of the service data plane.

eirikb commented 3 years ago

I'm aware you would essentially be admin, but I tried to do malicious things, and didn't manage to do much (I don't consider broadcast a problem here).
Kicking out clients though, that's a cool one, didn't see that was possible - I only looked at the JS side of it, and it seemed to only work for "your client" and since I couldn't list clients I couldn't get the ID of others anyway.

Will it be possible to connect to PubSub without a token in the future? How about using MSAL to get a token? (A bit bothersome for "guest" or non-user approaches though). Since User is optional when creating a token I really find it hard to understand why this flow is mandatory. You might consider Functions a splendid approach, but not having to need a function would in my honest opinion be a great option.

eirikb commented 3 years ago

Just tried setting expiresIn to a larger value, and 16y (or 5999d) works, that might be a viable solution. Just need a postit:

Hello self, remember to create a new token for that app you created 16 years ago

vicancy commented 3 years ago

Since User is optional when creating a token I really find it hard to understand why this flow is mandatory.

I think the main responsibility of this workflow is to prevent malicious users. I understand that you'd like some "anonymous" connect feature. The main concern for Web PubSub to allow anonymous connection is that it is a persistent connection. Web PubSub has limitations to concurrent connections, for example, free instances allow 20 concurrent connections, Unit 100 allows 100K concurrent connections. It is comparatively heavier than the "anonymous request to a web app".

If we allow anonymous connect, the issues we need to solve are:

  1. How to prevent the anonymous clients from using up the concurrent connection quota
  2. How to identify if the anonymous clients are expected or malicious and if they are malicious how to stop them

I understand your concern about the dependency on some server-side. Definitely we'd like to improve that too! We actually had several rounds of internal discussions about this too, to simplify the workflow.

There are several proposals, but the design is not yet finalized and is open for discussion:

  1. have a "allow anonymous" check box, similar to what web app has
  2. allow the client to connect with some specific client cert (e.g. thumbprint)
  3. allow the client to connect with RBAC
  4. allow the client from some specific IP to connect

Would love your feedback and thoughts!

eirikb commented 3 years ago

I think the main responsibility of this workflow is to prevent malicious users.

How does code like this actually help to prevent that? I just have to spam that endpoint with random values for id to get new clients 🤷

I don't have any good solutions in mind.
How about just allowing anonymous connections?

I do like MSAL, is it possible to create guest/anonymous logins there? I haven't done anything like that before. That would allow you on your side to have more control of origin and type of connection/token.
Tried to google around for "MSAL" and "guest user" but since B2B is essentially guest users it is a bit difficult.

eirikb commented 3 years ago

If the middleware becomes optional, how would join/leave messages be distributed?
My understanding is that these are now controlled by some kind of web hook (Event Handlers in Settings).

Could this be distributed automatically to all the clients who are in the same group(s)? Perhaps controlled by a role.

vicancy commented 3 years ago

How does code like this actually help to prevent that?

Ah, that one is a simple demo to show some basic idea, a more real-world case would be your application logic to decide if the client has permission to be connected or so. Some advanced auth samples are: https://github.com/Azure/azure-webpubsub/tree/main/samples/javascript/githubchat, or this https://github.com/benc-uk/chatr

For the guest/anonymous login, could you describe your scenarios a little bit more? Is that for a quicker getting started or your scenario wants anonymous connections? Do you want anonymous connections always connected to the service? If for example, anonymous connections use up the quota of concurrent connections, what do you expect, auto-scaling the service?

eirikb commented 3 years ago

Ah, that one is a simple demo to show some basic idea, ...

I understand it's a simple example, but the easiest samples to run and understand are probably the most copied ones.
The other sample, chatr, also creates tokens from querystring so it has the same issue. Of course it does, it is basically allowing anonymous users.

For the guest/anonymous login, could you describe your scenarios a little bit more?

What I really want is to not have a middleware. This would in some sense be allowing anonymous users, but providing a user id from the client would be fine.
How about if the service included:

The chatr sample "api" part is in my eyes a good example of what could have been part of the service, and can be viewed as boilerplate code.

vicancy commented 3 years ago

What I really want is to not have a middleware

May I ask the reason for not having a middleware? Don't want a long-run server-side? Want one service to handle all application logic instead of having Web PubSub+Web App? Prefer code-free development?

eirikb commented 3 years ago

May I ask the reason for not having a middleware?

Optionally code-less for simple setups and kickstarting projects. Code-full for more advanced setups.

eirikb commented 3 years ago

Would it also be possible to include userId automatically when sending messages to groups?

In the samples all messages have to go via server in order to insert correct user. userId is already provided to the service, could this not be inserted by the service?
This would be important for a server-code-less solution.