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:
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.