Implementation and deployment of Azure AD App-to-App Authentication Between Azure Functions and Blazor WebAssembly

A month ago, I worked on a client project that required integrating Azure Functions into their existing Blazor WebAssembly application architecture. The client wanted to connect to an external API that relied on webhooks, and Azure Functions provided an ideal separation layer between their main application and the external service.

Azure Functions offer excellent scalability for operationally intensive tasks, making them well-suited for handling webhook processing and other background operations.

A massive shout out to Damien Bowden and Marc Rufer from isolutions, for their help in creating this.

In this blog, I’ll walk through the complete implementation, covering:

  • Infrastructure provisioning using Terraform
  • Deployment automation with Azure DevOps Pipelines
  • Service-to-service authentication using Azure AD Client Credentials flow
  • Real-time updates from Functions to the Blazor WebAssembly app via Azure SignalR Service

The key challenge was establishing secure, non-interactive authentication between the Azure Functions and the Blazor WebAssembly application. This required implementing OAuth2 Client Credentials flow with Azure AD to enable the Functions app to securely communicate status updates back to the main application.

Much of this implementation is based on the foundational work by Damien. His comprehensive articles on Azure AD app-to-app authentication provided the technical foundation for this solution:

Microsoft Entra ID App-to-App security architecture

Implementing OAuth2 Client Credentials flow App-to-App security using Azure AD (non-interactive)

Implementing OAuth2 App-to-App security using Azure AD from a web app

Infrastructure provisioning using Terraform

main-functions.tf

# Resource Group for functions
resource "azurerm_resource_group" "rg_functions" {
    name     = "rg-functions"
    location = "West Europe"
    tags     = merge(var.resource_group_tags, { description = "Azure functions resource group" })
}

resource "azuread_group" "role-rg-functions-contributor" {
    display_name            = format("ra-%s-contributor", azurerm_resource_group.rg_functions.name)
    description             = "Role group which grants contributor access to the Azure Functions resource group"
    prevent_duplicate_names = true
    security_enabled        = true
    members                 = [
        data.azurerm_client_config.current.object_id, # Your current user Azure ObjectId
        azuread_service_principal.azuredevops.object_id # Azure DevOps pipeline service principal
    ]
    lifecycle {
        # apart from setting initially; do not flag changes in members and owners as state change
        ignore_changes = [members, owners]
    }
}

resource "azuread_group" "perm-rg-functions-contributor" {
    display_name            = format("pm-%s-contributor", azurerm_resource_group.rg_functions.name)
    description             = "Permission group granting contributor rights to the group"
    members                 = [azuread_group.role-rg-functions-contributor.id]
    prevent_duplicate_names = true
    security_enabled        = true
    lifecycle {
        # apart from setting initially; do not flag changes in owners as state change
        ignore_changes = [owners]
    }
}

resource "azurerm_role_assignment" "azuredevops-rg-functions-contributor" {
    scope                = azurerm_resource_group.rg_functions.id
    role_definition_name = "Contributor"
    principal_id         = azuread_service_principal.azuredevops.id
}

resource "azurerm_service_plan" "functions_appplan" {
    name                                  = "appplan-functions"
    resource_group_name                   = azurerm_resource_group.rg_functions.name
    location                              = var.default_location
    os_type                               = "Linux"
    sku_name                              = "Y1" #THIS IS CRITICAL TO ENSURE DYNAMIC UPSCALING
}

resource "azurerm_storage_account" "sa_functions" { 
# Place to store the functions app(s) codebase(s)
    name                     = "functionssa"
    resource_group_name      = azurerm_resource_group.rg_functions.name
    location                 = "West Europe"
    account_tier             = "Standard"
    account_replication_type = "LRS"
    min_tls_version          = "TLS1_2"
    tags = merge(var.service_tags, {
        description = "Functions Runtime Storage"
    })
}  

A critical infrastructure problem I encountered was that Azure Function Apps with Consumption (dynamic scaling) plans cannot be deployed to Resource Groups that previously hosted other App Services or Function Apps with service plan types.

This limitation stems from how Azure maps application hosting plans to underlying infrastructure pools. When a Resource Group contains r different service plan SKUs (Consumption vs. App Service plans), Azure’s resource allocation conflicts prevent new deployments.

So I created a separate resource group for new dynamic functions app different application types:

  • One RG for the main Blazor WebAssembly app (App Service Plan)
  • Another RG for Azure Functions (Consumption Plan)

Read more at : https://learn.microsoft.com/en-us/answers/questions/1538122/deployment-issue-with-azure-app-service-with-terra

main-functionsapp.tf

resource "azurerm_linux_function_app" "functionsapp" {
    name                = "functionsapp"
    resource_group_name = azurerm_resource_group.rg_functions.name
    location            = "West Europe"

    storage_account_name       = azurerm_storage_account.sa_functions.name
    storage_account_access_key = azurerm_storage_account.sa_functions.primary_access_key
    service_plan_id            = azurerm_service_plan.functions_appplan.id
    https_only                 = true

    tags = merge(var.service_tags, { description = "Functions App" })

    identity {
        type = "SystemAssigned"
    }

    site_config {
        application_stack {
            dotnet_version = "8.0"
            use_dotnet_isolated_runtime = "true"
        }
        application_insights_connection_string = azurerm_application_insights.appi.connection_string

        health_check_path   = "/api/health"
        health_check_eviction_time_in_min = 10

        http2_enabled = true
    }

    app_settings = {
        # Azure AD Configuration for the FUNCTIONS API
        "AzureAd__TenantId"                     = var.tenant_id
        "AzureAd__ClientId"                     = azuread_application.functionsaadapp.client_id
        "AzureAd__Instance"                     = "https://login.microsoftonline.com/"
        "AzureAd__Authority"                    = "https://login.microsoftonline.com/${var.tenant_id}"

        "BlazorWasmApp__ClientId"               = azuread_application.aadapp.client_id
        "BlazorWasmApp__AppUrl"                 = var.system_domain_uri
        "BlazorWasmApp__Scopes"                 = "api://BlazorWasmApp-${var.stage}/.default",
        "BlazorWasmApp__TokenEndpoint"          =  "https://login.microsoftonline.com/${var.tenant_id}/oauth2/v2.0/token"

        # Key Vault Configuration
        "AzureKeyVaultEndpoint"                 = azurerm_key_vault.kv.vault_uri

        # Application Insights
        "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.appi.connection_string

        WEBSITE_RUN_FROM_PACKAGE = ""
        WEBSITE_MOUNT_ENABLED    = "true"
    }

    lifecycle {
        ignore_changes = [
            app_settings["WEBSITE_RUN_FROM_PACKAGE"],
            app_settings["WEBSITE_MOUNT_ENABLED"]
        ]
    }
}

resource "azurerm_role_assignment" "functions-app-kv-reader-ra" {
    scope                = azurerm_key_vault.kv.id
    role_definition_name = "Key Vault Secrets User"
    principal_id         = azurerm_linux_function_app.functionsapp.identity[0].principal_id
}


resource "azuread_application" "functionsaadapp" {
    display_name     = "Functions AAD App"
    identifier_uris  = ["api://functionsaadapp"]
    sign_in_audience = "AzureADMyOrg"

    api {
        requested_access_token_version = 2
    }

    # Required_resource_access in azuread_application defines the API permissions
    # that this application needs to access other Azure services or Microsoft Graph APIs.
    # It's essentially the "permissions manifest" for the app.
    # Request permission to call the main Blazor Wasm app
    required_resource_access {
        # Functions app REQUESTS the permission
        resource_app_id = azuread_application.aadapp.client_id
        resource_access {
            id          = random_uuid.random-uuid-app-role-functions.result # The app role ID from main app
            type        = "Role"
        }
    }
}

resource "azuread_service_principal" "functions-app-sp" {
    client_id = azuread_application.functionsaadapp.client_id
}

# Grant the calling app  (functions) permission to access the target app (BlazorWasmApp)
resource "azuread_app_role_assignment" "aadapproleassignment-functions-app" {
    app_role_id          = random_uuid.random-uuid-app-role-functions.result
    principal_object_id  = azuread_service_principal.-functions-app-sp.object_id # Calling App 
    resource_object_id   = azuread_service_principal.aadapp-sp.object_id # Target App
}

resource "azuread_application_password" "functionsapppwd" {
    display_name      = "apppwd"
    application_id    = azuread_application.functionsaadapp.id
    end_date_relative = "17520h"
}

resource "azurerm_key_vault_secret" "functionsapppwd-secret" {
    key_vault_id = azurerm_key_vault.kv.id
    name         = "FunctionsApp--ClientSecret"
    value        = azuread_application_password.functionsapppwd.value
}

main-aad.tf

resource "azuread_application" "aadapp" {
  display_name     = "Blazor WASM AAD Application"
  identifier_uris  = ["api://blazorwasmapp"]
  sign_in_audience = "AzureADMyOrg"
  api {
    requested_access_token_version = 2
  }
  
  # Other configurations...

  # This is the app role that the functions app will be assigned to
    app_role {
        allowed_member_types = ["Application"]
        description          = "Allow access to main Blazor Wasm application"
        display_name         = "BlazorWasmAccess"
        enabled              = true
        id                   = random_uuid.random-uuid-app-role-functions.result
        value                = "BlazorWasmAccess"
    }
}

Deployment with Azure DevOps Pipelines

functionsapp.yml

name: FunctionsApp$(Date:yyyyMMdd)$(Rev:r)
trigger:
    batch: true
    branches:
        include:
            - main
    paths:
        include:
            - src

variables:
    isMainBranch: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
    artifactName: "FunctionsApp"
    buildProjects: "src/Functions/FunctionsApp.csproj"
    DOTNET_CLI_TELEMETRY_OPTOUT: true
    NUGET_PACKAGES: "$(Pipeline.Workspace)/.nuget/packages"

pool:
    vmImage: ubuntu-latest

stages:
    - stage: Build
      displayName: Build and Package
      dependsOn: []
      jobs:
          - template: jobs-build-functions.yml
            parameters:
                artifactName: $(artifactName)
                buildProjects: $(buildProjects)


    - stage: DeployProd
      displayName: Deploy to Prod
      dependsOn: Build
      condition: and(succeeded(), eq(variables.isMainBranch, true))
      jobs:
          - template: jobs-deploy-functions.yml
            parameters:
                artifactName: $(artifactName)
                environment: Production
                environmentCode: Prod
                serviceConnection: azuredevops-service-connection
                functionsAppName:  functionsapp
                keyVaultName: kv 

jobs-build-functions.yml

parameters:
    - name: buildProjects
      type: string

    - name: publishProjects
      type: string

    - name: buildConfiguration
      type: string
      default: "Release"

    - name: artifactName
      type: string
      default: "FunctionsApp"

jobs:
    - job: Build
      displayName: Build Azure Functions
      pool:
          vmImage: ubuntu-latest

      steps:
          #  Get .NET SDK
          - task: UseDotNet@2
            displayName: Get DotNet SDK
            inputs:
                packageType: sdk
                useGlobalJson: true

          # Build solution
          - task: DotNetCoreCLI@2
            displayName: Build Azure Functions
            inputs:
                command: build
                projects: ${{ parameters.buildProjects }}
                arguments: "--configuration ${{ parameters.buildConfiguration }} --property:VersionSuffix=$(Build.BuildNumber)"


          # Publish functions app
          - task: DotNetCoreCLI@2
            displayName: Publish Azure Functions
            inputs:
                command: publish
                projects: ${{ parameters.publishProjects }}
                publishWebProjects: false
                arguments: "--configuration ${{ parameters.buildConfiguration }} --property:VersionSuffix=$(Build.BuildNumber) --output $(Build.ArtifactStagingDirectory)/publish"
                zipAfterPublish: true


          - publish: $(Build.ArtifactStagingDirectory)/publish
            artifact: ${{ parameters.artifactName }}
            displayName: 'Publish Function App artifact'

jobs-deploy-functions.yml

parameters:
    - name: "artifactName"
      type: string
    - name: "environment"
      type: string
    - name: "environmentCode"
      type: string
    - name: "serviceConnection"
      type: string
    - name: "functionsAppName"
      type: string
    - name: "keyVaultName"
      type: string

jobs:
    - deployment: Deploy
      displayName: "Deploy ${{parameters.functionsAppName}}"
      environment: ${{parameters.environment}}
      strategy:
          runOnce:
              deploy:
                  steps:
                      - checkout: self
                      - download: current
                        displayName: Artifacts - Download function app artifact
                        artifact: ${{ parameters.artifactName }}

                      - task: AzureFunctionApp@2
                        displayName: Deploy Azure Function App
                        inputs:
                            azureSubscription: ${{parameters.serviceConnection}}
                            appType: functionAppLinux
                            appName: ${{parameters.functionsAppName}}
                            package: $(Pipeline.Workspace)/${{ parameters.artifactName }}/**/*.zip

I use Workload Identity Federation for my pipelines, for more information I recommend checking out Marc Rufer’s blog posts about it:

[HOWTO] Automate Terraform execution in Azure DevOps YAML pipeline

[HOWTO] Create Azure DevOps Service Connections with authentication method Workload Identity Federation using Terraform

Service-to-service authentication using Azure AD Client Credentials flow

I had a very torrid time here, as connecting the two apps in a non interactive manner was proving more difficult than I had anticipated.
Essentially the calling functions application successfully acquires a token with all expected claims (including roles, azp, azpacr), but when the token reaches the target Blazor Wasm app, the claims collection was empty. The token acquisition logs show 16 claims including the required app role on the functions side, but the main app’s JWT Bearer authentication results in zero claims being available to the authorization handlers. 

Having searched endlessly and worked poor Claude to death, I finally fell upon this : https://github.com/AzureAD/microsoft-identity-web/issues/547

The original application had:

// Authentication for user tokens
services.AddMicrosoftIdentityWebAppAuthentication(configuration)
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph("https://graph.microsoft.com/v1.0", initialScopes)
    .AddDistributedTokenCaches(); 

This handles interactive user login via OpenID Connect
and set OpenID Connect as the primary authentication method. It was designed for the users accessing the web application through browsers, to authenticate with.

What the functions app need to authenticate with was: Service Authentication

// Authentication for client tokens
services.AddAuthentication()
     .AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"))
     .EnableTokenAcquisitionToCallDownstreamApi(); 

This handles JWT Bearer tokens from service principals (Azure Functions)
by adding JWT Bearer as a named authentication scheme.

By having both authentication way, I was able to resolve conflicting authentication schemes.
Instead of having the Azure Functions app sent JWT Bearer tokens, and the Blazor Wasm app trying to process them through the web app authentication pipeline (OpenID Connect), which expected interactive user sessions rather than service principal tokens.

This configuration created separate, non-interfering pathways:

  • Browser requests → OpenID Connect → User authentication
  • API requests with Bearer tokens → JWT Bearer → Service authentication

To tell the app to use the service authentication all I need was to add:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

to the desired endpoints.

Getting the token

FunctionsToAppHttpClientFactory.cs

public class FunctionsToAppHttpClientFactory
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly FunctionsToAppTokenService _functionsToAppTokenService;
    private readonly ILogger<FunctionsToAppHttpClientFactory> _logger;

    public FunctionsToAppHttpClientFactory(IHttpClientFactory httpClientFactory, FunctionsToAppTokenService functionsToAppTokenService, ILogger<FunctionsToAppHttpClientFactory> logger)
    {
        _httpClientFactory = httpClientFactory;
        _functionsToAppTokenService = functionsToAppTokenService;
        _logger = logger;
    }

    public async Task<HttpClient> CreateClientAsync(Functions function)
    {
        _logger.LogInformation("Creating HTTP client for function: {Function}", function);

        // Create an http client and set the connection base address
        var client = _httpClientFactory.CreateClient(function.ToString());
        var accessToken = await _functionsToAppTokenService.GetApiTokenAsync(function);

        _logger.LogInformation("Token acquired, setting authorization header");

        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        return client;
    }
} 

FunctionsToAppTokenService.cs

public class FunctionsToAppTokenService
{
    private readonly IConfidentialClientApplication _app;
    private readonly IConfiguration _configuration;
    private readonly ILogger<FunctionsToAppTokenService> _logger;
    private readonly ClientCredentialsFlowService _clientCredentialsFlow;

    public FunctionsToAppTokenService(IConfiguration configuration, ILogger<FunctionsToAppTokenService> logger,
        ClientCredentialsFlowService clientCredentialsFlow)
    {
        _configuration = configuration;
        _logger = logger;
        _clientCredentialsFlow = clientCredentialsFlow;

        var tenantId = configuration["AzureAd:TenantId"];
        // Initialize MSAL client
        // Use the credentials of the functions app not the Blazor WASM app
        _app = ConfidentialClientApplicationBuilder
            .Create(_configuration["AzureAd:ClientId"])
            .WithClientSecret(_configuration["AzureAd:ClientSecret"])
            .WithAuthority($"https://login.microsoftonline.com/{tenantId}/")
            .Build();
    }

    public async Task<string> GetApiTokenAsync(Functions function)
    {
        var accessToken = await _clientCredentialsFlow.GetFromCacheAsync(function.ToString());

        if (accessToken != null)
        {
            _logger.LogInformation("Cached token found. Expires: {ExpiresIn} ({ExpiresKind}), Current UTC: {UtcNow}, Comparison: {IsValid}",
                accessToken.ExpiresIn,
                accessToken.ExpiresIn.Kind,
                DateTime.UtcNow,
                accessToken.ExpiresIn > DateTime.UtcNow);

            if (accessToken.ExpiresIn > DateTime.UtcNow)
            {
                return accessToken.AccessToken;
            }
        }

        var newAccessToken = await GetApiTokenClientAsync();
        _clientCredentialsFlow.AddToCache(function.ToString(), newAccessToken);
        return newAccessToken.AccessToken;
    }
    private async Task<ClientCredentialsFlowService.AccessTokenResult> GetApiTokenClientAsync()
    {
        try
        {
            var scopes = new[] { _configuration["BlazorWasmApp:Scopes"]};
            _logger.LogInformation("Requesting token with scope: '{Scope}'", scopes[0]);

            var authResult = await _app.AcquireTokenForClient(scopes).ExecuteAsync();

            if (authResult == null)
            {
                throw new Exception("Failed to acquire access token - null result");
            }

            // Decode the token to see what Azure AD actually returned
            try
            {
                var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
                var jsonToken = handler.ReadJwtToken(authResult.AccessToken);

            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to decode JWT token from Azure AD");
            }

            _logger.LogInformation("Successfully acquired access token");

            return new ClientCredentialsFlowService.AccessTokenResult
            {
                AccessToken = authResult.AccessToken,
                ExpiresIn = authResult.ExpiresOn.DateTime
            };
        }
        catch (MsalServiceException ex)
        {
            _logger.LogError(ex, "MSAL service exception: {ErrorCode} - {Message}", ex.ErrorCode, ex.Message);
            throw new Exception($"Authentication failed: {ex.ErrorCode} - {ex.Message}");
        }
    }
} 

ClientCredentialsFlowService.cs

public class ClientCredentialsFlowService
{
    private readonly IDistributedCache _cache;
    private const int CACHE_EXPIRATION_DAYS = 1;

    public ClientCredentialsFlowService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public void AddToCache(string key, AccessTokenResult accessTokenItem)
    {
        var options = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromDays(CACHE_EXPIRATION_DAYS));
        var serializedItem = JsonSerializer.Serialize(accessTokenItem);
        _cache.SetStringAsync(key, serializedItem, options);
    }

    public async Task<AccessTokenResult?> GetFromCacheAsync(string key)
    {
        var item = await _cache.GetStringAsync(key);
        return item != null ? JsonSerializer.Deserialize<AccessTokenResult>(item) : null;
    }

    public class AccessTokenResult
    {
        public string AccessToken { get; set; } = string.Empty;
        public DateTime ExpiresIn { get; set; }
    }

    public class AccessTokenItem
    {
        [JsonPropertyName("access_token")]
        public string AccessToken { get; set; } = string.Empty;

        [JsonPropertyName("expires_in")]
        public int ExpiresIn { get; set; }

        [JsonPropertyName("token_type")]
        public string TokenType { get; set; } = string.Empty;

        [JsonPropertyName("scope")]
        public string Scope { get; set; } = string.Empty;
    }
} 

FunctionsToAppHttpClientFactory.cs

FunctionsToAppTokenService.cs

ClientCredentialsFlowService.cs

should all be registered in the Functions app program.cs ONLY, as scoped.
I also encountered some token caching issues where previously cached tokens with incorrect configuration values were being reused after fixing the Azure AD settings. The cached tokens contained invalid claim values from the earlier misconfiguration period, causing silent authentication failures. Rebooting the Redis cache cleared these problematic cached tokens and forced fresh token acquisition with the correct configuration values.

Webhook Processing Flow

WebhookService.cs

public class WebhookService
{
    private readonly string _environment;
    private readonly ILogger<WorkflowWebhookService> _logger;
    private readonly FunctionsToAppHttpClientFactory _functionsToAppHttpClientFactory;
    private readonly string _appUrl;
    public WorkflowWebhookService( IConfiguration configuration, ILogger<WorkflowWebhookService> logger, FunctionsToAppHttpClientFactory functionsToAppHttpClientFactory)
    {
        _logger = logger;
        _functionsToAppHttpClientFactory = functionsToAppHttpClientFactory;
        _appUrl = configuration.GetValue<string>("BlazorWasmApp:AppUrl");
    }

    public async Task ProcessWebhookCall(string body)
    {
        var responseModel = await ProcessWebhookAsync(body);
        var message = responseModel.Message;

        var functionsToAppClient = await _functionsToAppHttpClientFactory.CreateClientAsync(Functions.Workflows);

        await functionsToAppClient.PostAsJsonAsync($"{_appUrl}/api/notifications", message);
    }

    private async Task<WebhookCallbackModel> ProcessWebhookAsync(string body)
    {
        var data = JsonSerializer.Deserialize<WebhookCallbackModel>(body);
        if (data is null)
        {
            throw new Exception(
                statusCode: HttpStatusCode.Unauthorized,
                message: "Failed to deserialize webhook response");
        }

        return data;
    }
} 

When the external API sends a webhook notification, the Azure Functions app processes the incoming data and forwards status updates to the main Blazor WebAssembly application using the secure authentication pattern described above.
The Functions app acts as an intermediary layer, transforming webhook payloads into structured notifications that can be consumed by the Blazor WebAssembly application’s real-time update system.

Real-time updates from Functions to the Blazor WebAssembly app via Azure SignalR Service

Before getting started with this, take a look at my post on how to add real-time notifications in Blazor WebAssembly with Azure SignalR Service. Once you follow those steps, the next phase becomes super easy.

SignalRNotificationsController.cs

[ApiController]
[Route("api/notifications")]
public class SignalRNotificationsController : ControllerBase
{
    private readonly ILogger<SignalRNotificationsController> _logger;
    private readonly AzureSignalRNotificationService _azureSignalRNotificationService;

    public SignalRNotificationsController(ILogger<SignalRNotificationsController> logger, AzureSignalRNotificationService azureSignalRNotificationService)
    {
        _logger = logger;
        _azureSignalRNotificationService = azureSignalRNotificationService;
    }

    [HttpPost]
    [Authorize(Policy = "ValidateAccessWorkflowsAppPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> ReceiveNotification([FromBody] NotificationModel notification)
    {
        _logger.LogInformation("Received task notification: {TaskId} - {Status}",
            notification.TaskId, notification.Status);

        await _azureSignalRNotificationService
            .SendStatusUpdateAsync(
                notification.UserOid,
                notification.TaskId,
                notification.Status,
                notification.Message);

        return Ok();
    }
}
public class NotificationModel
{
    public string Status { get; set; } = string.Empty;
    public string Message  { get; set; } = string.Empty;
    public string UserOid  { get; set; } = string.Empty;
    public string TaskId  { get; set; } = string.Empty;
}

Add the controller to your Blazor WASM app, and the notification model into a shared directory accessible by the Blazor app and the Function app.

With all this in place, you have enabled real-time push notifications from the Functions app to connected Blazor WebAssembly clients, completing the end-to-end webhook processing pipeline!

Conclusion

Implementing secure app-to-app authentication between Azure Functions and Blazor WebAssembly applications requires careful consideration of both infrastructure design and authentication architecture.

Infrastructure Separation: Azure Function Apps with Consumption plans require dedicated Resource Groups to avoid service plan conflicts. This separation also aligns with resource lifecycle management best practices.

Authentication Pipeline Design: The dual authentication setup using both AddMicrosoftIdentityWebAppAuthentication and AddMicrosoftIdentityWebApi enables a single application to handle both interactive user sessions and service-to-service authentication without conflicts.

Token Management: Proper token caching with appropriate expiration handling is crucial for performance, but cache invalidation becomes critical when configuration changes occur. Always ensure cached tokens align with current configuration values.

Configuration Management: Environment-specific configuration mismatches can cause subtle authentication failures. Verify that Azure AD application settings match exactly between development and production environments.

1 Comment.

Leave a Reply

Your email address will not be published. Required fields are marked *