Tag: Power Platform

  • Setting up Azure AD Authentication for a React SPA and .NET API with Dataverse On-Behalf-Of Flow

    Setting up Azure AD Authentication for a React SPA and .NET API with Dataverse On-Behalf-Of Flow

    This guide covers how to configure Entra authentication for a React Single Page Application (SPA) and a .NET API backend, enabling access to Microsoft Dataverse using the On-Behalf-Of (OBO) flow.

    1. Register the Backend API Application

    Name: myapp-api

    • Navigate to Azure Portal → Azure Active Directory → App registrations → New registration.
    • Name: myapp-api
    • Supported account types: Single tenant
    • Redirect URI: leave blank.

    After registration:

    Screenshot of the Azure portal "Expose an API" configuration page for the myapp-api app registration. The Application ID URI is set to api://4ac715bd-612a-4830-89e0-b3ad0efe5a6e. One scope is defined: user_impersonation, with "Admins and users" able to consent, display name "Access Dataverse data," and the state set to "Enabled."
    1. Expose an API:
      • Set the Application ID URI to: api://<myapp-api-app-id>
      • Add a Scope:
        • Name: user_impersonation
        • Display Name/Description: “Access Dataverse data”
        • State: Enabled
    2. Grant permissions:
      • Go to API permissions → Add a permission.
      • Choose APIs my organization uses, search for Dataverse or Dynamics CRM.
      • Add the user_impersonation or .default scope.
      • Grant admin consent
    Azure Active Directory API permissions setup for Dataverse integration. "user_impersonation" delegated permission from Dynamics CRM API is configured and granted for tenant. No admin consent required. Used for On-Behalf-Of (OBO) authentication flow in Azure AD-secured .NET APIs accessing Dataverse.

    Note: Dataverse App ID: 00000007-0000-0000-c000-000000000000

    2. Register the Frontend SPA Application

    Name: myapp-frontend

    • Navigate to Azure Portal → Azure Active Directory → App registrations → New registration.
    • Name: myapp-frontend
    • Supported account types: Single tenant
    • Redirect URIs:
      • Platform: Single-page application (SPA)
      • Add: http://localhost:3000 or any URLs your frontend is running on.

    After registration:

    1. Enable Implicit grant (under Authentication):
      • ID tokens
      • Access tokens
    2. Assign API permissions:
      • My APIs → select myaapp-api → add user_impersonation, you can search for the Application ID of myapp-api
    Azure AD API permissions setup for React SPA using Microsoft Identity platform. Application configured with Microsoft Graph "User.Read" and custom myapp-api "user_impersonation" delegated permissions for Dataverse access via On-Behalf-Of authentication flow.

    3. Pre-authorize Frontend in Backend

    Azure AD authorized client applications setup showing pre-authorized React SPA frontend (Client ID: 3915cbe2-24b3-41f6-8337-4b0c7ccf4664) for backend API access using user_impersonation scope in On-Behalf-Of authentication flow.
    • In myapp-api, go to Expose an API → Add a client application.
    • – Add:
      • Client ID: 3915cbe2-24b3-41f6-8337-4b0c7ccf4664 (myapp-frontend App ID)
      • Scope: user_impersonation

    4. Register the Backend App as a Dataverse App User

    • Copy the myapp-api Application ID.
    • Go to Power Platform Admin Center → App Users → New App User
    • Paste the Application ID.
    • Assign the appropriate security role for Dataverse access.

    5. Backend Code Setup

    Program.cs

    // Choose credential source based on environment (development vs production)
    TokenCredential credential = builder.Environment.IsDevelopment() switch
    {
        true => new DefaultAzureCredential(),
        false => new DefaultAzureCredential()
    };
    
    // Configure authentication using Microsoft Identity Web
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi()
        .AddDownstreamApi("DownstreamDataverseApi", builder.Configuration.GetSection("DownstreamDataverseApi"))
        .AddInMemoryTokenCaches();
    
    // Configure HTTP client for Dataverse API calls
    builder.Services.AddHttpClient("DataverseClient", client =>
    {
        client.BaseAddress = new Uri(dataverseUrl); // Dataverse environment URL
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        client.DefaultRequestHeaders.Add("OData-Version", "4.0");
        client.DefaultRequestHeaders.Add("Prefer", "odata.include-annotations=\"*\""); // Request annotations
    })
    // Add authentication handler for injecting tokens
    .AddHttpMessageHandler(_ => new DataverseManagedIdentityAuthHandler())
    // Optional: Add resilience features (retry policies, etc.)
    .AddStandardResilienceHandler();
    
    // Register custom Dataverse client wrapper
    builder.Services.AddScoped<DataverseHttpClient>();
    
    var app = builder.Build();
    
    // Enable authentication and authorization middleware
    app.UseAuthentication();
    app.UseAuthorization();

    appsettings.json

    {
        "AzureAd": {
            "Audience": "api://4ac715bd-612a-4830-89e0-b3ad0efe5a6e", // Backend API Application ID URI
            "Authority": "https://login.microsoftonline.com/<your-tenant-id>", // Tenant Authority URL
            "Instance": "https://login.microsoftonline.com/", // Common Microsoft login endpoint
            "TenantId": "<your-tenant-id>", // Azure AD Tenant ID
            "Domain": "your-tenant-domain.com", // Tenant domain name
            "ClientId": "4ac715bd-612a-4830-89e0-b3ad0efe5a6e" // Backend API App Registration Client ID
        },
        "DownstreamDataverseApi": {
            "BaseUrl": "https://{your-environment}.api.crm.dynamics.com/", // Dataverse API base URL
            "Scopes": "https://{your-environment}.api.crm.dynamics.com/.default" // Dataverse API default scope
        }
    }
    

    DataverseManagedIdentityAuthHandler.cs

    // Custom HTTP message handler for authenticating Dataverse API requests
    public sealed class DataverseManagedIdentityAuthHandler(
        ITokenAcquisition tokenAcquisition,
        IHttpContextAccessor httpContextAccessor,
        IConfiguration configuration) : DelegatingHandler
    {
        // This method is called for every outgoing HTTP request
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            // Get the current authenticated user from HTTP context
            var user = httpContextAccessor.HttpContext?.User;
    
            // Reject if no authenticated user is found
            if (user?.Identity is not { IsAuthenticated: true })
            {
                throw new UnauthorizedAccessException("User is not authenticated");
            }
    
            // Load required scopes from configuration
            var scopesToAccessDataverseApi = configuration["DownstreamDataverseApi:Scopes"]
                ?? throw new Exception("Configuration 'DownstreamDataverseApi:Scopes' is missing.");
    
            // Acquire access token on behalf of the authenticated user
            var token = await tokenAcquisition.GetAccessTokenForUserAsync(
                [scopesToAccessDataverseApi],
                user: user);
    
            // Attach the token to the outgoing HTTP request
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    
            // Proceed with sending the HTTP request
            return await base.SendAsync(request, cancellationToken);
        }
    }
    
  • Accessing Dataverse OData API from Azure Functions Using Managed Identity

    Accessing Dataverse OData API from Azure Functions Using Managed Identity

    When building serverless solutions in Azure, securely connecting to services like Microsoft Dataverse (formerly Common Data Service) is a common task. Azure Functions combined with Managed Identity offers a clean and secure way to authenticate without managing secrets. In this post, I’ll show you how to access the Dataverse OData API using a managed identity, and share a few gotchas—like needing the Application (client) ID, not the Object ID, when registering the app in Power Platform.

    Why Managed Identity?

    Managed Identity lets your Azure Function authenticate against Azure AD and access resources like Dataverse without needing any client secrets or certificates.

    Step 1: Configure Azure Function for Managed Identity

    In your Azure Function:

    1. Go to the Identity blade in the portal.
    2. Enable the System-assigned managed identity.
    3. Copy the Object ID (you’ll need this in the next step).

    Step 2: Register the Managed Identity as an App User in Power Platform

    Here’s the tricky part: Power Platform (Dataverse) requires the Application (client) ID of an Entra ID Enterprise Application, not the Object ID.

    Here’s how to get it:

    1. Go to Microsoft Entra ID → Enterprise Applications.
    2. Search for the Object ID from your Azure Function’s Identity blade.
    3. Open that Enterprise App and copy the Application ID (this is what Dataverse wants).
    4. In Power Platform Admin Center, create a new App User, paste this Application ID, and assign the correct security role.

    Step 3: Implement a Custom Auth Handler in Your Azure Function

    Now, let’s get to code.

    Create a delegating handler that uses DefaultAzureCredential to fetch a token for Dataverse:

    public sealed class DataverseManagedIdentityAuthHandler : DelegatingHandler
    {
        private readonly DefaultAzureCredential _credential = new();
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var scopes = new[] { "https://{your-environment}.api.crm.dynamics.com/" };
    
            var token = await _credential.GetTokenAsync(
                new TokenRequestContext(scopes),
                cancellationToken);
    
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
    
            return await base.SendAsync(request, cancellationToken);
        }
    }

    Step 4: Register the HTTP Client with Dependency Injection

    Here’s a clean way to set up your Dataverse client using IHttpClientFactory:

    public static IServiceCollection AddDataverseHttpClient(this IServiceCollection services, string dataverseUrl)
    {
        services.AddHttpClient("DataverseClient", client =>
            {
                client.BaseAddress = new Uri(dataverseUrl);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
                client.DefaultRequestHeaders.Add("OData-Version", "4.0");
                client.DefaultRequestHeaders.Add("Prefer", "odata.include-annotations=\"*\"");
            })
            .AddHttpMessageHandler(_ => new DataverseManagedIdentityAuthHandler())
            .AddStandardResilienceHandler(); // optional
    
        services.AddScoped<DataverseHttpClient>(); // your custom wrapper
    
        return services;
    }