asp.netazure-managed-identityazure-appservice

Use managed identity on a single endpoint in AzureAppService


I have an ASP.Net app that is handling users with a user name and password authentication. It's working fine and I want minimal disruption. I have a new requirement to make a calculation it does available to a FunctionApp. I could just copy the database connection and code over, but that seems like a maintainability problem. Can I set up a single endpoint on that same ASP.NET app to allow a FunctionApp to call with a Managed Identity?

Everything I have found so far is using ManagedIdentity for the entire application.

example:
/api/login Generates a JWT for users to login with username and passsword. Anonymous access allowed
/api/Bar accessed by users with JWT
/api/Baz accessed by users with JWT

api/Foo accessed by WebApp with Entra
is this possible?


Solution

  • Use managed identity on a single endpoint in AzureAppService

    Yes, it is possible to use managed identity on a single endpoint in Azure App service.

    I created Asp. Net Core web API with two endpoints api/bar accessed by user credentials and api/foo accessed by Managed Identity. Created an Azure Function that calls the api/foo endpoint using its System Assigned Managed Identity.

    In Azure I created App registration and Expose an api as shown below:

    enter image description here

    and Created Azure Web App and Azure Function App and assigned System assigned Managed Identity to Function app.

    enter image description here

    In code we need below Values:

     var tenantId = "<TeanantId>";
     var apiAppClientId = "<ClientID>";
     var expectedCallerClientId = "<ApplicationId>";
    

    you can find ApplicationId in Function app Enterprise Application as shown below: enter image description here

    API Controller Code:

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.IdentityModel.Protocols;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.IdentityModel.Tokens;
    using System.IdentityModel.Tokens.Jwt;
    namespace ManagedIdentityApi.Controllers;
    [ApiController]
    [Route("api")]
    public class SecureController : ControllerBase
    {
        [HttpGet("bar")]
        [Authorize]
        public IActionResult Bar()
        {
            return Ok($"Hello {User.Identity?.Name}, you are authorized via JWT!");
        }
        [HttpGet("foo")]
        [AllowAnonymous]
        public async Task<IActionResult> Foo()
        {
            var authHeader = Request.Headers["Authorization"].FirstOrDefault();
            if (authHeader == null || !authHeader.StartsWith("Bearer "))
                return Unauthorized("Missing or invalid Authorization header.");
            var token = authHeader.Substring("Bearer ".Length);
            var tenantId = "<TeanantId>";
            var apiAppClientId = "<ClientID>";
            var expectedCallerClientId = "<ApplicationId>";
            var v1Issuer = $"https://sts.windows.net/{tenantId}/";
            var v2Issuer = $"https://login.microsoftonline.com/{tenantId}/v2.0";
            var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
                $"{v2Issuer}/.well-known/openid-configuration",
                new OpenIdConnectConfigurationRetriever());
            var config = await configManager.GetConfigurationAsync();
            var tokenHandler = new JwtSecurityTokenHandler();
            try
            {
                var validationParams = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuers = new[] { v1Issuer, v2Issuer }, 
                    ValidateAudience = true,
                    ValidAudiences = new[] { $"api://{apiAppClientId}" },
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKeys = config.SigningKeys
                };
                var principal = tokenHandler.ValidateToken(token, validationParams, out _);
                var appid = principal.FindFirst("appid")?.Value;
                if (appid != expectedCallerClientId)
                    return Forbid("Caller is not the expected application.");
                return Ok($"MSI Token validated. Caller App ID: {appid}");
            }
            catch (Exception ex)
            {
                return Unauthorized($"Token validation failed: {ex.Message}");
            }
        }
    }
    

    Program.cs:

    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.IdentityModel.Tokens;
    using System.Text;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllers();
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = "yourapp",
                ValidateAudience = true,
                ValidAudience = "yourapp",
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes("ThisIsAReallyLongSecureJwtKey123456789!"))
            };
        });
    builder.Services.AddAuthorization();
    var app = builder.Build();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
    

    Function.cs Code:

    using System.Net.Http.Headers;
    using Azure.Identity;
    using Azure.Core;
    using Microsoft.Azure.Functions.Worker;
    using Microsoft.Extensions.Logging;
    public class CallApiFunction
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger _logger;
    
        public CallApiFunction(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpClientFactory.CreateClient();
            _logger = loggerFactory.CreateLogger<CallApiFunction>();
        }
        [Function("CallApiFunction")]
        public async Task RunAsync([TimerTrigger("0 */5 * * * *")] TimerInfo timerInfo)
        {
            var credential = new DefaultAzureCredential();
            var apiClientId = "api://<ClientID>";
            TokenRequestContext requestContext = new(new[] { $"{apiClientId}/.default" });
            AccessToken accessToken = await credential.GetTokenAsync(requestContext);
            var request = new HttpRequestMessage(HttpMethod.Get, "https://<AzureWebName>.canadacentral-01.azurewebsites.net/api/foo");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
            var response = await _httpClient.SendAsync(request);
            var content = await response.Content.ReadAsStringAsync();
            _logger.LogInformation($"API Response: {response.StatusCode} - {content}");
        }
    }
    

    Function Program.cs:

    using Microsoft.Azure.Functions.Worker.Builder;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    var builder = FunctionsApplication.CreateBuilder(args);
    builder.Services.AddHttpClient(); 
    builder.ConfigureFunctionsWebApplication();
    builder.Build().Run();
    

    Azure Output: enter image description here

    enter image description here