Wednesday, August 14, 2024

Azure APIM Policy: Maintain CORS Allowed-Origins per Environment using Named Values

In this post let's see how we can maintain  CORS policys' allowed-origins in Azure API Management (APIM) per environment.

In APIM, the CORS policy looks like this:
<cors allow-credentials="true">
  <allowed-origins>
    <origin>https://localhost:4200</origin>
    <origin>https://sub-1.domain.net</origin>
    <origin>https://sub-2.domain.net</origin>
  </allowed-origins>
  <allowed-methods>
    <method>*</method>
  </allowed-methods>
  <allowed-headers>
    <header>*</header>
  </allowed-headers>
  <expose-headers>
    <header>*</header>
  </expose-headers>
</cors>
Most of the time, allowed-origins will be different in each environment. For example,  in a Production environment, we don't want to allow https://localhost:4200

We can manage these using named values.

Let's add a named value of type Plain as follows.
web-allowed-origins named value
Value is basically comma-separated origins.

And now, we can modify the CORS policy as below:
<cors allow-credentials="true">
  <allowed-origins>
    <origin>@{
      string[] allowedOrigins = "{{web-allowed-origins}}"
          .Replace(" ", string.Empty)
          .Split(',');
      string requestOrigin = context.Request.Headers.GetValueOrDefault("Origin", "");
      bool isAllowed = Array.Exists(allowedOrigins, origin => origin == requestOrigin);
      return isAllowed ? requestOrigin : string.Empty;
    }</origin>
  </allowed-origins>
  <allowed-methods>
    <method>*</method>
  </allowed-methods>
  <allowed-headers>
    <header>*</header>
  </allowed-headers>
  <expose-headers>
    <header>*</header>
  </expose-headers>
</cors>
And now the policy doesn't contain any environment-specific values. In different APIM environments, you can have different values for web-allowed-origins named value.

Imagine, you want to allow https://localhost:4200https://*.domain.net. You can further customize the policy by doing something like the following.
<cors allow-credentials="true">
    <allowed-origins>
      <origin>@{
        string[] allowedOrigins = "{{web-allowed-origins}}"
            .Replace(" ", string.Empty)
            .Split(',');
        string requestOrigin = context.Request.Headers.GetValueOrDefault("Origin", "");
        bool isAllowed = Array.Exists(allowedOrigins, origin =>
        {
            if (origin.Trim() == requestOrigin)
            {
                return true;
            }
             if (origin.Contains("*"))
            {
                string[] originParts = origin.Split('.');
                string[] requestOriginParts = requestOrigin.Split('.');
              
                if (originParts.Length != requestOriginParts.Length)
                {
                    return false;
                }
              
                for (int i = 0; i < originParts.Length; i++)
                {
                    if (originParts[i] == "https://*")
                    {
                       continue;
                    }
              
                    if (originParts[i] != requestOriginParts[i])
                    {
                        return false;
                    }
                }
              
                return true;
            }
              
            return false;
        });
          
        return isAllowed ? requestOrigin : string.Empty;
    }</origin>
    </allowed-origins>
    <allowed-methods>
      <method>*</method>
    </allowed-methods>
    <allowed-headers>
      <header>*</header>
    </allowed-headers>
    <expose-headers>
      <header>*</header>
    </expose-headers>
  </cors>
Hope this helps.

Happy Coding.

Regards,
Jaliya

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

Monday, August 5, 2024

Azure APIM Wildcard Operations

I recently had a requirement where I wanted to expose some endpoints in an ASP.NET Core Web API via an Azure APIM, but those were not included in the APIs OpenAPI specification. These endpoints/operations were dynamically being added by a 3rd party library. 

With these operations not defined in the APIM, the consumers cannot reach the API/Backend. And I didn't want to add each of these 3rd party operations individually, and fortunately APIM supports Wildcard operations.

We can put wildcard operations by suffixing the URL with /*, something like below:
APIM: Add operation
This will route any GET operation under /reports/viewer/ to corresponding API/Backend.

And now I can't have these operations manually added to APIM, and wanted to add them into applications OpenAPI specification. I was using Swagger, so I can easily add a IDocumentFilter, something like the following.
public class DevExpressReportingDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument openApiDocumentDocumentFilterContext context)
    {
        var reportViewerOperation = new OpenApiOperation
        {
            Summary = "Report Viewer",
            Tags =
            {
                new OpenApiTag { Name = "Reporting" }
            },
            Responses =
            {
                { "200"new OpenApiResponse() }
            }
        };

        var reportDesignerOperation = new OpenApiOperation
        {
            Summary = "Report Designer",
            Tags =
            {
                new OpenApiTag { Name = "Reporting" }
            },
            Responses =
            {
                { "200"new OpenApiResponse() }
            }
        };

        openApiDocument?.Paths.Add("/reports/viewer/*"new OpenApiPathItem()
        {
            Operations =
            {
                { OperationType.Get, reportViewerOperation },
                { OperationType.Post, reportViewerOperation }
            }
        });

        openApiDocument?.Paths.Add("/reports/designer/*"new OpenApiPathItem()
        {
            Operations =
            {
                { OperationType.Get, reportDesignerOperation },
                { OperationType.Post, reportDesignerOperation }
            }
        });
    }
}
And once these endpoints are available in OpenAPI specification, I can update the deployment of the API to import the OpenAPI specification to APIM which would result in something like this.
APIM: Wildcard Operations
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, July 30, 2024

Azure Container Apps: Scaling Rule based on Azure Service Bus Subscription

In this post, let's see how to set a scaling rule for Azure Container App to scale based on # of messages in an Azure Service Bus Subscription.

I am deploying an Azure Function App with a ServiceBusTrigger as a Container App.

# Windows PowerShell
az containerapp create `
--name <CONTAINER_APP_NAME> `
--resource-group <CONTAINER_APP_RESOURCE_GROUP>` --environment <CONTAINER_APP_ENVIRONMENT>
--image <CONTAINER_APP_IMAGE> `
--set-env-vars "AzureWebJobsServiceBus=<SERVICE_BUS_CONNECTION_STRING>" `
--min-replicas 0 `
--max-replicas 5 ` --secrets "service-bus-connectionstring=<SERVICE_BUS_CONNECTION_STRING>"
--scale-rule-name some-subsciption-name-rule `
--scale-rule-type azure-servicebus `
--scale-rule-metadata `
    "subscriptionName=some-subsciption-name" `
    "topicName=some-topic-name" `
    "messageCount=10" `
--scale-rule-auth "connection=service-bus-connectionstring"

Here I am creating a container app with a scale rule of type azure-servicebus. An important thing to notice is, I am creating a secret service-bus-connectionstring and using it for scale-rule-auth, so KEDA scaler can communicate with the Service Bus namespace.

Metrics: Replica Count

More reading:
   Set scaling rules in Azure Container Apps

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, July 17, 2024

EF Core 8.0: Numeric Rowversion for Azure SQL/Microsoft SQL Server

In this post, let's have a look at this small yet handy EF Core 8.0 feature for troubleshooting concurrency issues.

Before EF Core 8.0, the rowversion property in C# classes needs to be of type byte[].
public record Post
{
    public int Id { getset}

    public string Title { getset}

    public byte[] Timestamp { getset}
}
And when debugging, it looks like the following.
Timestamp as byte[]
Now with EF Core 8.0, we can map the rowversion to long or ulong.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(e => e.Timestamp)
        .HasConversion<byte[]>()
        .IsRowVersion();
}
public record Post
{
    public int Id { getset}

    public string Title { getset}

    public long Timestamp { getset}
}
When debugging it's more readable now.
Timestamp as long
Happy Coding.

Regards,
Jaliya

Monday, July 15, 2024

Introducing dotnet nuget why

With .NET SDK 8.0.4xx and later versions, we will have access to a   new dotnet nuget command: dotnet nuget why. You can try this now with the latest .NET 9 SDK Preview: 9.0.100-preview.6 (thanks @ErikEJ for pointing it out).
dotnet nuget why --help
dotnet nuget why --help
For an example,
dotnet nuget why <PROJECT|SOLUTION> System.Text.Json
dotnet nuget why
I can see the dependency graph for the given package and if it references an old package.

Read more:

Happy Coding.

Regards,
Jaliya

Thursday, July 11, 2024

Received Microsoft MVP Award in Developer Technologies

I am humbled and honored once again to receive the precious Microsoft Most Valuable Professional (MVP) Award for the 11th consecutive year.

As always looking forward to another great year on top of Microsoft Development Stack.
Microsoft Most Valuable Professional (MVP)
Thank you Microsoft for your appreciation and Thank you everyone for your continuous support.

Happy Coding.

Regards,
Jaliya

Tuesday, July 2, 2024

Azure DevOps Pipeline: Build and Deploy Azure Container App

In this post, let's see how we can build and deploy an Azure Container App from an Azure DevOps Pipeline.

Here for deployment, I am using az containerapp update.

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

variables:
  acrServiceConnection: <ACR_SERVICE_CONNECTION>
  acrName: myacr.azurecr.io
  imageRepositoryName: '<ACR_REPOSITORY_NAME>'
  containerAppServiceConnection: '<CONTAINER_APP_SERVICE_CONNECTION>'
  containerAppResourceGroup: '<CONTAINER_APP_RESOURCE_GROUP>'
  containerAppName: '<CONTAINER_APP_NAME>'

name: $(Build.BuildId)

stages:
stage: Build
  displayName: Build
  jobs:  
  - job: Build
    displayName: Build Docker Image
    steps:
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        containerRegistry: '$(acrServiceConnection)'
        repository: '$(imageRepositoryName)'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'
        buildContext: './'
        tags: '$(Build.BuildId)'

stage: Deploy
  displayName: Deploy
  dependsOn:
  - Build
  condition: succeeded('Build')
  jobs:  
  - deployment: Deployment
    displayName: Deploy to Container App
    # Requires an environment named 'Development'
    environment: Development
    strategy:
      runOnce:
        deploy:
          steps:
           - task: AzureCLI@2
             displayName: Update Container App
             inputs:
               azureSubscription: '$(containerAppServiceConnection)'
               scriptType: 'bash'
               scriptLocation: 'inlineScript'
               inlineScript: |
                 az containerapp update \
                 --name $(containerAppName) \
                 --resource-group $(containerAppResourceGroup) \
                 --image '$(acrName)/$(imageRepositoryName):$(Build.BuildId)' \
                 --set-env-vars \
                   'MongoDB__ConnectionString=<VALUE>' \
                   'ServiceBus__ConnectionString=<VALUE>' \
                 --min-replicas 1 \
                 --max-replicas 1

Azure DevOps already has an Azure Container Apps Deployment Task AzureContainerApps@1, which I haven't used, but do check it out.

Hope this helps.

Happy Coding.

Regards,
Jaliya