Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Friday, August 9, 2024

Azure APIM as a Negotiate Server for Azure SignalR Service

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()
    },
    transportsignalR.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. POST: https://{some-signalr-service}.service.signalr.net/client/negotiate, the returned URL from previes call). This is where actual Negotiation happens with Azure SignalR Service. The response contains connectionId, which identifies the connection on the server and the list of transports that the server supports.

2. Negotiate with Azure SignalR Service
3. WebSocket connection to GET: wss://{some-signalr-service}.service.signalr.net/client/?hub={myHubName}&id={connectionToken}&access_token={accessToken}

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,

  1. Acquire a token using a managed identity to communicate with SignalR Service
  2. Call SignalR Services'  generateToken endpoint using the token for managed identity (mi-access-token)
  3. Call the backend using the generated token (client-access-token)
  4. 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()
    },
    transportsignalR.HttpTransportType.WebSockets,
};

this.hubConnection = new signalR.HubConnectionBuilder()
    .withUrl("https://{some-apim}.azure-api.net/client"options)
    .withAutomaticReconnect()
    .build();

await this.hubConnection.start();
And yes, it does.
1. Negotiate with Azure SignalR Service via APIM
2. WebSocket connection via APIM
SignalR Connected
Hope this helps.

More read:
   Azure SignalR Service: How to use Azure SignalR Service with Azure API Management
   Azure SignalR Service: Client negotiation

Happy Coding.

Regards,
Jaliya

Wednesday, February 1, 2017

Getting Started with React, Babel and Webpack

React is getting popular everyday and I believe you can find many articles to know what React is. In this post let’s see how we can setup a HelloWorld React application using some set of nice tools.

To build this sample, I am going to use following set of tools and libraries.
This is the editor that I am going to use, and of course, you can use any editor you prefer. But I suggest you try out Visual Studio Code as it offers great features for JavaScript developers. All these features are made possible by the new JavaScript Language Service (Code name : “Salsa”)  which is also getting shipped with Visual Studio 2017.
I am going to use npm as the JavaScript package manager.
Babel is the transpiler here. We can use all the great features in ECMAScript 6, also known as ECMAScript 2015 (ES6/ES2015) and Babel will make sure they will get transpiled to ES5 which will work on all browser versions.
Webpack is the module bundler that I am using, it will be responsible for effectively bundling the files based on the configuration I provide.
I will be using webpack-dev-server to test and host my application. It provides nice features such as hot reloading. We just need to modify the code and changes will get reflected. We don’t need to refresh the browser.

Now it’s time to write some code. I will start by installing the global dependencies that I need. I am going to install webpack-dev-server as a global npm package.
npm install webpack-dev-server -g
Now I am creating a folder for the application “HelloWorldReact” and inside there creating another folder named “app” and that’s where I am going to have all my code files.

From the “HelloWorldReact” folder, I am opening up a command prompt and running the npm init command to create the package.json file. On the prompts, I am just keeping the default values as it is.

Now let’s install the packages using npm.

dependencies
npm install react react-dom --save
devDependencies
npm install webpack babel-core babel-loader file-loader react-hot-loader babel-preset-es2015 babel-preset-react --save-dev
As you already know upon the installation, package.json will get updated. I am further modifying the package.json as follows adding a script.
{
  "name": "helloworldreact",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start-webpack-server": "webpack-dev-server --hot --inline --colors --progress"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^15.4.2",
    "react-dom": "^15.4.2"
  },
  "devDependencies": {
    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.22.0",
    "file-loader": "^0.9.0",
    "react-hot-loader": "^1.3.1",
    "webpack": "^1.14.0"
  }
}
Here, the packages listed under dependencies are only needed when running the application in production. The packages listed under both dependencies and devDependencies are need when developing the application. Under devDependencies, I have installed,
  • Core Babel package
  • Babel loader for Webpack
  • Two presets to make sure ES6/ES2015 and react transpilation
  • File loader for Webpack
  • Webpack & react-hot-loader to enable webpack dev servers hot loading for React (webpack-dev-server should be running with --hot flag).
The script is for running webpack-dev-server with some set of flags. To know about what these flags are visit webpack-dev-server CLI.

Now let’s create a js file to configure webpack. I am naming it as webpack.config.js.

webpack.config.js
var path = require('path');
var webpack = require('webpack');
 
module.exports = {
  context: path.join(__dirname, 'app'),
  entry: {
    javascript: './app.js',
    html: './index.html'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  devServer: {
    inline: true,
    port: 9999
  },
  module: {
    loaders: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react']
        }
      },
      {
        test: /\.html$/,
        loader: "file?name=[name].[ext]",
      }
    ]
  }
};
Here I have specified the entry files, bundle output directory, devServer information and loaders. And for js files, I have set the loader as Babel. So what happens in the js loader here is when you come across a path that resolves to a '.js' inside of a require()/import statement, use the babel-loader to transform it before you add it to the bundle. For more information about webpack loaders, go to https://webpack.github.io/docs/loaders.html.

Now let’s create the entry files. Navigate to the “app” folder, and let’s create a app.js and index.html files.

app.js
import React from 'react';
import ReactDOM from 'react-dom';
 
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello World</h1>
      </div>
    )
  }
}
 
ReactDOM.render(<App />, document.getElementById('main'))
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello World</title>
</head>
<body>
  <div id="main">
  </div>
 
  <script src="/bundle.js"></script>
</body>

</html>
Now I am almost good. From the command prompt run the following command.
npm run start-webpack-server 
Now you should be able to see that bundling is happening and webpack dev server is starting. Once completed, navigate to http://localhost:9999 and if all is good you should see something like below.

image
Output
Now try changing the text inside our React component and just save the file, you should be able to see the changes in the browser without refreshing the browser.

Still the bundled files are not outputted to the destination (HelloWorldReact/dist). When you are ready for the deployment, from the command prompt, you can run webpack -d command to output files.

I have made the code available on GitHub. Feel free to fork.
https://github.com/jaliyaudagedara/Blog-Post-Samples/tree/master/HelloWorldReact

Happy Coding.

Regards,
Jaliya