Setup authentication using AAD B2C for an Azure Function (ASP.Net Core 9 isolated worker model)

Alexandre Guimond 20 Reputation points
2025-03-10T16:23:59.9133333+00:00

I'm building a web app that calls a function app, hosted in Azure. Web app users authenticate using AAD B2C. I have followed these directions to create the web app and app registrations in my B2C tenant. This is working and the web app is able to retrieve a token with the required scope. I'm also able to passes the access token as a bearer token in the authentication header of the HTTP request to the function app by using an Authorization header.

As the function app will be accessible from outside my Azure environment (e.g. from mobile app), I'm wondering how to setup the authentication on the function app side so it reads and validates the token and scope. If this was a standard web API a I could use these instructions. But since I'm using an Azure Function I'm unsure what my class should look like, and the changes to the program.cs of my Azure Function project.

Should my program.cs for the Azure Function initiate the authentication library as in:

new HostBuilder()
	.ConfigureFunctionsWebApplication()
    .ConfigureServices((context, services) =>
    {
        IConfiguration configuration = context.Configuration;
        ...
        services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApi(configuration, "AzureAdB2C");
        services.AddAuthorization();
        ...
    })
    .Build()
    .Run();

I feel I'm missing something because the IHost returned by Build() doesn't have a UseAuthentication() I can call as for a WebApplication.

And should my function app (the Azure function that needs authentication) be :

using ...
...
namespace ...
{
    [Authorize]
    [RequiredScope("ReadWrite.All")]
    public class FhirProxy
    {
        ...
		
        [Function("fhir")]
        public async Task<HttpResponseData> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", "put", "delete", "patch", Route = "fhir/{*path}")] HttpRequestData req)
        {
		...
		}
	}
}

Using this, although I'm able to authenticate a user and call the azure function from the web app and the resulting headers includes the Authorization token, when debugging my function, I can see that the provided identity is not authenticated (isAuthenticated == false) and does not contain any claims.

Might there exist an example of how a function app can read and validate the token and scope provided by the web app call?

I've also seen that it's possible to use a build-in authentication feature ("Easy Auth"). But this approach seems to be an all or nothing approach, and I have Azure Functions in my project that do not require authentication. I'm also unsure if this works well in the context of AAD B2C as I don't want the user to be redirected to the authentication page when the function app is called; I want the function app to read and validate the token passed from the web app (or mobile app). If this is the right approach, might there be an recent example that uses B2C?

Azure Functions
Azure Functions
An Azure service that provides an event-driven serverless compute platform.
5,909 questions
{count} votes

Accepted answer
  1. RithwikBojja 3,055 Reputation points Microsoft External Staff Moderator
    2025-03-11T08:59:26.7833333+00:00

    Hi @Alexandre Guimond,

    To validate scope and token in .Net 9 Isolated Functions, I use below code which does the ask:

    
    using Microsoft.Azure.Functions.Worker;
    
    using Microsoft.Azure.Functions.Worker.Middleware;
    
    using Microsoft.Extensions.DependencyInjection;
    
    using Microsoft.Extensions.Hosting;
    
    using Microsoft.Extensions.Configuration;
    
    using Microsoft.Azure.Functions.Worker.Http;
    
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    
    using Microsoft.IdentityModel.Protocols;
    
    using Microsoft.IdentityModel.Tokens;
    
    using System.IdentityModel.Tokens.Jwt;
    
    using System.Net;
    
    using System.Security.Claims;
    
    var builder = new HostBuilder()
    
        .ConfigureFunctionsWebApplication(workerBuilder =>
    
        {
    
            workerBuilder.UseMiddleware<TestMidWare>();  
    
        })
    
        .ConfigureServices((context, services) =>
    
        {
    
            services.AddSingleton<TestMidWare>();
    
        });
    
    var rith = builder.Build();
    
    rith.Run();
    
    public class TestMidWare : IFunctionsWorkerMiddleware
    
    {
    
        private readonly IConfiguration ri_cfg;
    
        private static readonly Dictionary<string, IEnumerable<SecurityKey>> SigningKeysCache = new();
    
        public TestMidWare(IConfiguration configuration)
    
        {
    
            ri_cfg = configuration;
    
        }
    
        public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    
        {
    
            var testreq = await context.GetHttpRequestDataAsync();
    
            if (testreq != null)
    
            {
    
                var authHeader = testreq.Headers.GetValues("Authorization")?.FirstOrDefault();
    
                Console.WriteLine($"Authorization Header: {authHeader}");
    
                if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
    
                {
    
                    var token = authHeader.Substring(7);  
    
                    var ri_prince = await ValidateTokenAsync(token);  
    
                    if (ri_prince != null)
    
                    {
    
                        var rescp = ri_cfg["AzureAd:RequiredScope"];
    
                        var rsclm = ri_prince.FindFirst("http://schemas.microsoft.com/identity/claims/scope")?.Value;  
    
                        Console.WriteLine($"Scope Claim: {rsclm}, Required Scope: {rescp}");
    
                        if (!string.IsNullOrEmpty(rsclm) && rsclm.Split(' ').Contains(rescp))
    
                        {
    
                            context.Items["User"] = ri_prince;  
    
                            await next(context); 
    
                            return;
    
                        }
    
                        var forbiddenResponse = testreq.CreateResponse(HttpStatusCode.Forbidden);
    
                        await forbiddenResponse.WriteStringAsync("Hello Rithwik, Forbidden: Missing required scope.");
    
                        return;
    
                    }
    
                }
    
            }
    
            var unauthorizedResponse = testreq?.CreateResponse(HttpStatusCode.Unauthorized);
    
            if (unauthorizedResponse != null)
    
            {
    
                await unauthorizedResponse.WriteStringAsync("Hello Rithwik, Unauthorized: Invalid or missing token.");
    
            }
    
        }
    
        private async Task<ClaimsPrincipal?> ValidateTokenAsync(string token)
    
        {
    
            var tokenHandler = new JwtSecurityTokenHandler();
    
            var jwtToken = tokenHandler.ReadJwtToken(token);
    
            var issuer = jwtToken.Issuer;
    
            bool isB2C = issuer.Contains("b2clogin.com");
    
            var validAudiences = new[]
    
            {
    
                "https://infrab2c.onmicrosoft.com/44d9ca62",
    
                "44d9cf03a62"
    
            };
    
            string tenantId = ri_cfg["AzureAd:TenantId"];
    
            string authority = isB2C
    
                ? ri_cfg["AzureAdB2C:Authority"]
    
                : $"https://login.microsoftonline.com/{tenantId}/v2.0";
    
            try
    
            {
    
                var validationParameters = new TokenValidationParameters
    
                {
    
                    ValidateIssuer = true,
    
                    ValidIssuers = new[] { authority, issuer },
    
                    ValidateAudience = true,
    
                    ValidAudiences = validAudiences,
    
                    ValidateIssuerSigningKey = true,
    
                    IssuerSigningKeys = await GetSigningKeysAsync(issuer),
    
                    ValidateLifetime = true,
    
                    ClockSkew = TimeSpan.Zero
    
                };
    
                return tokenHandler.ValidateToken(token, validationParameters, out _);
    
            }
    
            catch (Exception ex)
    
            {
    
                Console.WriteLine($"Token validation failed: {ex}");
    
                return null;
    
            }
    
        }
    
        private static async Task<IEnumerable<SecurityKey>> GetSigningKeysAsync(string issuer)
    
        {
    
            if (SigningKeysCache.TryGetValue(issuer, out var keys))
    
            {
    
                return keys;
    
            }
    
            try
    
            {
    
                var ri_cfg_man = new ConfigurationManager<OpenIdConnectConfiguration>(
    
                    $"{issuer}/.well-known/openid-configuration",
    
                    new OpenIdConnectConfigurationRetriever());
    
                var openIdConfig = await ri_cfg_man.GetConfigurationAsync();
    
                keys = openIdConfig.SigningKeys;
    
                SigningKeysCache[issuer] = keys;
    
                return keys;
    
            }
    
            catch (Exception ex)
    
            {
    
                return Array.Empty<SecurityKey>();
    
            }
    
        }
    
    }
    
    

    Here in validAudiences used https://<your-tenant-name>.onmicrosoft.com/<application-client-id>

    and client id of app registration

    Function1.cs:

    
    using Microsoft.Azure.Functions.Worker;
    
    using Microsoft.Azure.Functions.Worker.Http;
    
    using Microsoft.Extensions.Logging;
    
    using System.Net;
    
    using System.Security.Claims;
    
    using System.Threading.Tasks;
    
    namespace FunctionApp2
    
    {
    
        public class Function1
    
        {
    
            private readonly ILogger<Function1> ri_lg;
    
            public Function1(ILogger<Function1> logger)
    
            {
    
                ri_lg = logger;
    
            }
    
            [Function("Function1")]
    
            public async Task<HttpResponseData> Run(
    
                [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
    
                FunctionContext context) 
    
            {
    
                ri_lg.LogInformation("Hello Rithwik, Starting the function");
    
                if (context.Items.TryGetValue("User", out var userObj) && userObj is ClaimsPrincipal user)
    
                {
    
                    var response = req.CreateResponse(HttpStatusCode.OK);
    
                    await response.WriteStringAsync($"Welcome {user.Identity?.Name} to Azure Functions!");
    
                    return response;
    
                }
    
                var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
    
                await unauthorizedResponse.WriteStringAsync("Unauthorized: Please provide a valid token.");
    
                return unauthorizedResponse;
    
            }
    
        }
    
    }
    
    

    local.settings.json:

    
        {
    
          "IsEncrypted": false,
    
          "Values": {
    
            "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    
            "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    
        
    
            "AzureAd:RequiredScope": "access_as_user",
    
            "AzureAd:TenantId": "be8d5d4",
    
            "AzureAd:ClientId": "44d9ca62"
    
          }
    
        }
    
    

    Output:

    enter image description here

    If you are generating token with Azure ADB2C then change local.settings.json to:

    
    {
    
      "IsEncrypted": false,
    
      "Values": {
    
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    
        "AzureAd:ClientId": "your-client-id",
    
        "AzureAdB2C:_Authority": "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/policyname/v2.0"
    
      }
    
    }
    
    

    Hope this helps.

    If the answer is helpful, please click Accept Answer and kindly upvote it. If you have any further questions about this answer, please click Comment.

    1 person found this answer helpful.

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.