I am working on an ASP.NET 8.0 Web API and reacts app projects. These projects are registered in azure Active Directory B2C (ADB2C) under different tenants. Please refer #region Authentication & Security
under full code snapshot
Tenant A: Web API project -> .NET 8.0 Web API
Tenant B: React Client App (consume APIs from Tenant A)
Tenant C: React Client App (consume APIs from Tenant A)
Tenant N: React Client App (consume APIs from Tenant A)
I need to make an API call from the React App to the .NET Web API, which is working fine. I have configure multiple clients in appsetting and seems all good,
however I am getting issue setting up Authority dynamically based on user from specific tenant. To test i put hardcore authority soon after
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
and it works
but it won't work if I place it after options.TokenValidationParameters
to configure based on client,
options.Authority = $"{client.Instance}/{client.TenantName}/{client.Policy}/v2.0/";
options.MetadataAddress = $"{options.Authority}.well-known/openid-configuration";
i believe is too late by then
, I need to dynamically configure Authority
based on client login
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Reflection;
using System.Security.Claims;
using TXN.GV.Application.BackOffice;
using TXN.GV.Domain.Entity;
using TXN.GV.Enterprise.Data.DataContexts;
namespace MyApp.Web.APIs.Configuration
{
public static class ServicesConfigurator
{
public static void Configure(WebApplicationBuilder builder, IConfiguration configuration)
{
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
#region Swagger
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1"
});
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization Bearer {token}",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
#endregion
#region Data - DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlConnectionString"));
});
#endregion
#region MediataR Container
//Global Visa Application BackOffice
var assemblyBackOfficeName = "TXN.GV.Application.BackOffice";
var assemblyBackOffice = Assembly.Load(assemblyBackOfficeName);
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(assemblyBackOffice));
#endregion
#region AutoMapper Configuration
builder.Services.AddAutoMapper(typeof(ApplicationAssembly));
#endregion
#region CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAnyOrigin", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
#endregion
#region Authentication & Security
var azureAdB2CConfig = builder.Configuration.GetSection("AzureAdB2C");
var clientID = builder.Configuration.GetSection("AzureAdB2C").GetSection("ClientId").Value;
var signUpInPolicy = builder.Configuration.GetSection("AzureAdB2C").GetSection("SignUpSignInPolicy").Value;
var tenantName = builder.Configuration.GetSection("AzureAdB2C").GetSection("TenantName").Value;
var clientsConfig = configuration.GetSection("AzureAdB2C:Clients").Get<List<ClientConfig>>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var tenantBInstance = "https://appOnline.b2clogin.com";
var tenantBName = "appOnline.onmicrosoft.com";
options.Authority = $"{tenantBInstance}/{tenantBName}/{signUpInPolicy}/v2.0/";
options.MetadataAddress = $"{options.Authority}.well-known/openid-configuration";
// Explicitly set the valid issuer
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidAudience = azureAdB2CConfig["ClientId"],
ValidateLifetime = true,
IssuerValidator = (issuer, securityToken, validationParameters) =>
{
var client = clientsConfig.FirstOrDefault(client =>
issuer.Equals($"{client.Instance}/{client.TenantId}/v2.0/", StringComparison.OrdinalIgnoreCase));
if (client != null)
{
//NEED HELP HERE
//options.Authority = $"{client.Instance}/{client.TenantName}/{client.Policy}/v2.0/";
//options.Authority = $"{tenantBInstance}/{tenantBName}/{signUpInPolicy}/v2.0/";
//options.MetadataAddress = $"{options.Authority}.well-known/openid-configuration";
Console.WriteLine($"Valid issuer found: {issuer}");
Console.WriteLine($"Dynamic Authority set: {options.Authority}");
return issuer;
}
else
{
Console.WriteLine($"Invalid issuer. Received: {issuer}");
foreach (var conf in clientsConfig)
{
Console.WriteLine($"Expected Issuer for debug: {conf.Instance}/{conf.TenantId}/v2.0/");
}
throw new SecurityTokenInvalidIssuerException($"Invalid issuer: {issuer}");
}
}
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = AuthenticationFailed,
OnMessageReceived = OnMessageReceived,
OnTokenValidated = OnTokenValidated
};
});
}
private static Task AuthenticationFailed(AuthenticationFailedContext context)
{
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
return Task.CompletedTask;
}
private static Task OnMessageReceived(MessageReceivedContext context)
{
var accessToken = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
context.Token = accessToken;
Console.WriteLine($"Token received: {context.Token}");
return Task.CompletedTask;
}
private static Task OnTokenValidated(TokenValidatedContext context)
{
var token = context.SecurityToken;
var claims = context.Principal.Claims;
try
{
Console.WriteLine($"User ID: {context.Principal.FindFirstValue(ClaimTypes.NameIdentifier)}");
if (context.Request.Path.HasValue)
{
var userClaims = new UserClaims();
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity == null || !claimsIdentity.Claims.Any()) { throw new ApplicationException("Identity shouldn't be null and must have claims."); }
else
{ userClaims = ExtractUserClaims(claimsIdentity); }
}
}
catch (Exception ex)
{
}
return Task.CompletedTask;
}
private static UserClaims ExtractUserClaims(ClaimsIdentity identity)
{
var userClaims = new UserClaims
{
UserId = Guid.TryParse(identity.FindFirst(ClaimTypes.NameIdentifier)?.Value, out Guid userId) ? userId : Guid.Empty,
Iss = identity.FindFirst("iss")?.Value,
FirstName = identity.FindFirst(ClaimTypes.GivenName)?.Value,
LastName = identity.FindFirst(ClaimTypes.Surname)?.Value,
DisplayName = identity.FindFirst("name")?.Value,
Email = identity.FindFirst("emails")?.Value,
IsUserAuthenticated = identity.IsAuthenticated,
IsUserNew = identity.HasClaim(claim => claim.Type == "newUser" && claim.Value == "true"),
IssuedAt = DateTimeOffset.FromUnixTimeSeconds(long.Parse(identity.FindFirst("iat")?.Value ?? "0")).UtcDateTime,
Expiration = DateTimeOffset.FromUnixTimeSeconds(long.Parse(identity.FindFirst("exp")?.Value ?? "0")).UtcDateTime,
NotBefore = DateTimeOffset.FromUnixTimeSeconds(long.Parse(identity.FindFirst("nbf")?.Value ?? "0")).UtcDateTime
};
return userClaims;
}
#endregion
}
public class ClientConfig
{
public string ClientId { get; set; }
public string TenantName { get; set; }
public Guid TenantId { get; set; }
public string Policy { get; set; }
public string Instance { get; set; }
}
}
appsetting.json
"AzureAdB2C": {
"Instance": "https://Machine.b2clogin.com",
"TenantName": "Machine",
"Tenant": "Machine.b2clogin.com",
"Domain": "Machine.onmicrosoft.com",
"ClientId": "00000000-0000-0000-0000-000000000008",
"TenantId": "00000000-0000-0000-0000-0000000000019",
"SignUpSignInPolicy": "B2C_1_SignUpIn",
"ClientSecret": "xxx",
"CallbackPath": "/signin-oidc",
"Scope": "Core.API.All",
"Clients": [
{
"ClientId": "00000000-0000-0000-0000-000000000007",
"TenantName": "FranceVisaOnline",
"TenantId": "00000000-0000-0000-0000-000000000006",
"Policy": "B2C_1_SignUpIn",
"Instance": "https://FranceVisaOnline.b2clogin.com"
},
{
"ClientId": "00000000-0000-0000-0000-000000000003",
"TenantName": "VisaOnline",
"TenantId": "00000000-0000-0000-0000-000000000004",
"Policy": "B2C_1_SignUpIn",
"Instance": "https://VisaOnline.b2clogin.com"
},
{
"ClientId": "00000000-0000-0000-0000-000000000002",
"TenantName": "ItalyVisaOnline",
"TenantId": "00000000-0000-0000-0000-000000000001",
"Policy": "B2C_1_SignUpIn",
"Instance": "https://ItalyVisaOnline.b2clogin.com"
}
]
},
I have found the answer from following video
https://www.youtube.com/watch?v=2rhhlwKO_Xw&t=38s