I have Azure functions app written in Node.js, that uses Azure SignalR Service in serverless mode for some raal-time functionality. For clients to be able to connect to this SignaR Service, my Azure functions expose the standard /negotiate endpoint, which provides SignalR Service connection info as follows:
const inputSignalR = input.generic({
type: "signalRConnectionInfo",
name: "connectionInfo",
hubName: "my-hub",
});
async function negotiate(request: HttpRequest, context: InvocationContext) {
try {
return { body: JSON.stringify(context.extraInputs.get(inputSignalR)) };
} catch (error) {
context.log(error);
return {
status: 500,
jsonBody: error,
};
}
}
app.post("negotiate", {
authLevel: "anonymous",
handler: negotiate,
route: "negotiate",
extraInputs: [inputSignalR],
});
this works as expected - SignalR clients call the negotiate endpoint, from which they get SignalR Service connection info (url and access token) and they use those to connect to the SignalR Service. This is standard SignalR connection flow and it works without problems.
The issue comes when I want to make that generated access token in connection info aware of the user (in the above state it does not contain user identity and without it SignaR Service does not know what user is connected to it). I need that to be able to send SignalR messages to specfic users.
Since our Azure functions (including the SignalR specific /negotiate one) are already called with our standard JWT token in Authorization header (for which we already have shared logic to verify and parse the claims), I would like to take the user identifier claim from our JWT token and use it as user identifier in that connection info access token.
According to docs there are a few ways to augment the generated SignalR Service access token by using additional input binding properties/settings:
userId - with this property, you can tell the generated access token to use a header field or query parameter as user identifier for SignalR. This however does not solve my issue, as I want the user identity to be taken from incoming JWT token, not header/query param. There is a possibility that our SignalR clients would put their user identifiers into an agreed header or query parameter, but I don't want to do that, as than a SignalR client could pretend to be somebody else by putting someone else's user identifier into that agreed header/query param, which is a security risk. Also it's kind of weird duplication - the verified user identity is already present in JWT token that the clients are sending, so why put it in some additional field.idToken + claimTypeList - with those properties it's possible to specify from where the incoming JWT token should be parsed (in our case its Authorization header) and which claims from it should be propagated to generated SignalR Service access token. This however does not help either as SignalR uses specific claim name (asrs.s.uid) in access token as user identifier. So even though I'm able to successfully propagate name, email and account_id claims from our JWT token into the SignalR access token, the SignalR does not recognize the user identity as it's not under expected claim name.In documentation I found that this is doable in C#, using Azure SignalR Service Management SDK or using Azure SignalR Service function extension. Using either of those you can specify the user identity "manually" in the function body, before returning the connection info.
This is exactly what I would need, as the logic for verifying and obtaining user identity from our standard JWT token already exists in our code base, the only thing missing is how to push it to SignalR access token.
I could not find anything like it for NodeJS Azure functions. Is is even possible?
There is no build-in way to inject custom claim from incoming third-party JWT into SignalR access token to be used as user identity claim for Node.js Azure functions. It needs to be done "manualy" on your own.
Based on https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-concept-client-negotiation#self-exposing-negotiate-endpoint I managed to inject custom user identity claim from our custom incoming JWT into SignaR access token as user identity claim (simplified version without error handling):
import { sign, decode } from "jws";
const inputSignalR = input.generic({
type: "signalRConnectionInfo",
name: "connectionInfo",
hubName: "my-hub",
});
async function negotiate(request: HttpRequest, context: InvocationContext) {
const signalRDefaultConnectionInfo = context.extraInputs.get(inputSignalR);
const signalRConnectionString = process.env.AzureSignalRConnectionString;
const signalRAccessKey = /AccessKey=(.*?);/.exec(signalRConnectionString)[1];
const userId = extractUserId(request); // extract user identity claim from your incoming JWT
const originalDecodedToken = decode(signalRDefaultConnectionInfo.accessToken);
const customizedToken = sign({
header: originalDecodedToken.header,
payload: {
...originalDecodedToken.payload,
"asrs.s.uid": userId, // claim used by SignalR Service to hold user identity
},
secret: signalRAccessKey
});
return { url: signalRDefaultConnectionInfo.url, accessToken: customizedToken };
}
app.post("negotiate", {
authLevel: "anonymous",
handler: negotiate,
route: "negotiate",
extraInputs: [inputSignalR],
});