aspnet / AzureSignalR-samples

Code samples for Azure SignalR
MIT License
327 stars 389 forks source link

[Question] [Serverless] How can I send a message to an individual client using REST API? #42

Closed mariomeyrelles closed 6 years ago

mariomeyrelles commented 6 years ago

Dear Sirs,

I have studied the examples and I fully understand the architecture of Azure SignalR. I was able to broadcast messages from Postman. Also, I was able to run a chat example and was able to exchange messages.

The issue is: in a serverless scenario, how would I identify the id of an individual connected user? How Javascript clients can obtain their ID on Azure SignalR Service? This is needed because I want to send individual messages to a given user when his/her command gets processed on the backend. For example, in my use case, I would use a Azure Function to process a command that can take minutes to be executed.

What I have tried:

Using trace log on javascript client side, I was able to find an ID during negotiation:

Information: SSE connected to https://accendis-wefinance.service.signalr.net:5001/client/?hub=chat&id=61Ztvpa0KEQeKkJBgB2ZPQ&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1MzEzNTY4OTcsImV4cCI6MTUzMTM1ODY5NywiaWF0IjoxNTMxMzU2ODk3LCJhdWQiOiJodHRwczovL2FjY2VuZGlzLXdlZmluYW5jZS5zZXJ2aWNlLnNpZ25hbHIubmV0OjUwMDEvY2xpZW50Lz9odWI9Y2hhdCJ9.NBFYcQaq-pl3c31AAeKmrLBIGnw-uON_MNWY0yMvJ_Y Utils.ts:165:20

So then I tried to build a new request using the id like this:

POST /api/v1-preview/hub/chat/user/61Ztvpa0KEQeKkJBgB2ZPQ HTTP/1.1
Host: accendis-wefinance.service.signalr.net:5002
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1MzEzMTgyMTUsImV4cCI6MTU2Mjg1OTQ5NCwiYXVkIjoiaHR0cHM6Ly9hY2NlbmRpcy13ZWZpbmFuY2Uuc2VydmljZS5zaWduYWxyLm5ldDo1MDAyL2FwaS92MS1wcmV2aWV3L2h1Yi9jaGF0L3VzZXIvNjFadHZwYTBLRVFlS2tKQmdCMlpQUSIsInN1YiI6IiJ9.aVhw0AyQ-WuT9NIymFzESn6Gv3R3zLedddqCngLzxFg
Cache-Control: no-cache
Postman-Token: 329b9cbd-8f86-4e62-8a3f-c5e23b8af025

{ "target": "newMessage0", "arguments": [ {
    "sender": "mario",
    "text": "API rest do SignalR"
} ] }

And as result, I receive a 202 but nothing happens on client side.

How can I see what really happened?

chenkennt commented 6 years ago

The id in url (61Ztvpa0KEQeKkJBgB2ZPQ) is the connection id. We don't support send message to connection id as it's not easy to get it in Azure function. Instead we support send message to a user id (/api/v1-preview/hub/chat/user). User id is the NameIdentifier claim in access token (this is something you can control when you're generating the access token in Azure function). A sample can be found here:

https://github.com/aspnet/AzureSignalR-samples/pull/39/files#diff-577b0ba7bbffc0f105ca22d9947f3191R33

Pay attention to GenerateAccessToken() which shows how to generate a token with user id.

mariomeyrelles commented 6 years ago

Thanks @chenkennt

It works!

For sake of completeness, here is an example of token generation (negotiation) Azure Function in Javascript. Claims.NameIdentifier maps to sub in jwt:

var jwt = require('jsonwebtoken')

module.exports = function (context, req, connectionInfo) {

    var token = {
            "iss": "MyService",
            "aud": connectionInfo.endpoint,
            "sub": req.query.uid
    }
    var secret = process.env["AzureSignalRKey"];
    var signed = jwt.sign(token,secret, { expiresIn: '48h' });
    connectionInfo.accessKey = signed;
    context.res = { body: connectionInfo };
    context.done();
};

On the client, the negotiation and connection process can be done like this:

function getConnectionInfo() {
    return axios.post(`${apiBaseUrl}/api/SignalRInfo?uid=${data.username}`, null, getAxiosConfig())
        .then(resp => resp.data);
}
getConnectionInfo().then(info => {
const options = {
    accessTokenFactory: () => info.accessKey,
    logMessageContent: false
};
const connection = new signalR.HubConnectionBuilder()
    .withUrl(info.endpoint, options)
    .configureLogging(signalR.LogLevel.Trace)
    .build();

connection.on('newMessage', newMessage);
connection.onclose(() => console.log('disconnected'));
console.log('connecting...');

connection.start()
    .then((data) => console.log('connected!', data))
    .catch(console.error);
}).catch(console.error);

Thanks for your help again!

chenkennt commented 6 years ago

Good to know it works, and thanks for the complete sample! 😃

jrmcdona commented 5 years ago

In this scenario what did you use for userId input binding on your negotiate Azure function?

shiweiwei114 commented 3 years ago

In this scenario what did you use for userId input binding on your negotiate Azure function?

I don't think @mariomeyrelles is putting any userId to the input binding for SignalR service, he was generating the access token by himself and adding the userId as part of the token signature.