In this post, let's see how to use Azure APIM as a Negioate Server for Azure SignalR Service.
Let's start with a background.
I have an Angular client application that uses microsoft/signalr to communicate with Azure SignalR Service and the negotiation is done over an Azure Function that uses SignalRConnectionInfoInput.
[Function("Negotiate")]
public async Task<HttpResponseData> Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData request,
[SignalRConnectionInfoInput(HubName = SignalR.StreamlineHub, UserId = "{headers.x-ms-signalr-userid}")] string connectionInfo)
{
// TODO: Read Authorization header and validate token
HttpResponseData response = request.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync(connectionInfo);
return response;
}
So basically before the client application can connect to Azure SignalR Service, it calls the above endpoint which will return the Azure SignalR Service endpoint URL and a valid access token. Then it starts communicating with the Azure SignalR Service using the chosen Transport method, in my case it's WebSockets.
let options = {
headers: {
'x-ms-signalr-userid': this.tenantUserId,
'x-authorization': 'Bearer ' + this.oidcSecurityService.getAccessToken()
},
transport: signalR.HttpTransportType.WebSockets,
};
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl("https://{some-azure-function}.azurewebsites.net/api", options)
.withAutomaticReconnect()
.build();
await this.hubConnection.start();
The main flow of events from the client application side,
1. POST: https://{some-azure-function}.azurewebsites.net/api/negotiate, to retrieve the Azure SignalR Service service endpoint URL and a valid access token.
1. Reteieve Azure SignalR Service URL and an access token |
2. Negotiate with Azure SignalR Service |
3. WebSocket Connection |
Now I needed to remove this Azure Function and instead expose Azure SignalR Service via APIM.
Let's start modifying APIM by adding the required APIs for WebSocket transport as follows.
1. Add a HTTP API:
- Display name: SignalR negotiate
- Web service URL: https://{some-signalr-service}.service.signalr.net/client/negotiate/
- API URL suffix: client/negotiate/
- Add two operations, and saving with the following parameters:
- negotiate preflight
- Display name: negotiate preflight
- URL: OPTIONS /
- negotiate
- Display name: negotiate
- URL: POST /
2. Add a WebSocket API:
- Display name: SignalR connect
- Web service URL: wss://{some-signalr-service}.service.signalr.net/client/
- API URL suffix: client/
Now the APIs are added, go to the Settings tab in each of these APIs and uncheck Subscription required.
Now let's configure the policies for these APIs.
1. HTTP API:
All Operations
<policies>
<inbound>
<cors allow-credentials="true">
<allowed-origins>
<origin>https://localhost:4200</origin>
<!-- TODO: Add other origins-->
</allowed-origins>
<allowed-methods>
<method>*</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>
<validate-jwt header-name="x-authorization" failed-validation-httpcode="401" failed-validation-error-message="Access token is missing or invalid." require-expiration-time="false">
<!--Read Authorization header and validate token, not part of this-->
</validate-jwt>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
negotiate
<policies>
<inbound>
<base />
<!--Step 1: Use a managed identity to get an access token for the SignalR service.-->
<authentication-managed-identity resource="https://signalr.azure.com"
client-id="{Managed_Identity_Client_ID}"
output-token-variable-name="mi-access-token"
ignore-error="false" />
<!--Step 2: Use the access token to get a SignalR client access token.--> <!--NOTE: In production environments, we don't want UserId to be in a HTTP header, instead extract from JWT etc.-->
<send-request mode="new" response-variable-name="tokenResponse" timeout="20" ignore-error="false">
<set-url>@("https://{some-signalr-service}.service.signalr.net/api/hubs/{my_hub_name}/:generateToken?api-version=2023-07-01&userId=" + context.Request.Headers.GetValueOrDefault("x-ms-signalr-userid",""))</set-url>
<set-method>POST</set-method>
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["mi-access-token"])</value>
</set-header>
</send-request>
<!--Step 3: Extract the client access token from the response and set it as a variable.-->
<set-variable name="client-access-token" value="@(((IResponse)context.Variables["tokenResponse"]).Body.As<JObject>()["token"].ToString())" />
<!--Step 4: Set the client access token as a header in the request to the SignalR service.-->
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["client-access-token"])</value>
</set-header>
<!--Step 5: Set the hub name in the query parameter.-->
<set-query-parameter name="hub" exists-action="override">
<value>{myhubName}</value>
</set-query-parameter>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!--Step 6: Modify the response adding the client access token to the response body.-->
<return-response>
<set-status code="200" reason="OK" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body template="none">@{
JToken body = context.Response.Body.As<JToken>();
body["accessToken"] = (string)context.Variables["client-access-token"];
return JsonConvert.SerializeObject(body, Newtonsoft.Json.Formatting.Indented);
}</set-body>
</return-response>
</outbound>
<on-error>
<base />
</on-error>
</policies>
Here in Step 1, I am using authentication-managed-identity. In order for this, I have modified Access Control (IAM) of my Azure SignalR Service granting SignalR Service Owner to the managed identity I am using.
So basically what's happening here as a summary,
- Acquire a token using a managed identity to communicate with SignalR Service
- Call SignalR Services' generateToken endpoint using the token for managed identity (mi-access-token)
- Call the backend using the generated token (client-access-token)
- Once the response is received, modify the response by adding an accessToken property with the value of the generated token (client-access-token)
2. WebSocket API:
SignalR connect
<policies>
<inbound>
<base />
<set-query-parameter name="hub" exists-action="override">
<value>{myhubName}</value>
</set-query-parameter>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
And that's about it.
Now the final step is modifying Angular client code to point to APIM and making sure it's getting connected.
let options = {
headers: {
'x-ms-signalr-userid': this.tenantUserId,
'x-authorization': 'Bearer ' + this.oidcSecurityService.getAccessToken()
},
transport: signalR.HttpTransportType.WebSockets,
};
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl("https://{some-apim}.azure-api.net/client", options)
.withAutomaticReconnect()
.build();
await this.hubConnection.start();
1. Negotiate with Azure SignalR Service via APIM |
2. WebSocket connection via APIM |
SignalR Connected |
More read:
Azure SignalR Service: How to use Azure SignalR Service with Azure API
Management
Azure SignalR Service: Client negotiation
Happy Coding.
Regards,
Jaliya
No comments:
Post a Comment