I am working on .net 8.0 web api and reacts typeScript app projects and registered in azure adb2c different tenant; Azure Active Directory B2C (ADB2C). I need help on validating signature where i have .net and react on different issuers. Please refer to #region Authentication & Security
for web api
ValidateIssuerSigningKey = true,
error
IDX10500: Signature validation failed. No security keys were provided to validate the signature.
Tenant A
: Web API .NET 8.0 project with scope https://TenantA.onmicrosoft.com/api/xxx/Core.API.All
Web API Config
"AzureAdB2C": {
"Instance": "https://tenantA.b2clogin.com",
"TenantName": "tenantA",
"Tenant": "tenantA.b2clogin.com",
"Domain": "tenantA.onmicrosoft.com",
"ClientId": "xxx",
"TenantId": "xxx",
"ClientSecret": "xxx",
"CallbackPath": "/signin-oidc",
"Scope": "Core.API.All"
},
Tenant B
: React Client App (this need to consume APIs from Tenant A) and under api permission i have permission granted Core.API.All
MSAL Configuration
export const MsalConfiguration = {
auth: {
clientId: "xxx",
authority: "https://tenantB.b2clogin.com/tenantB.onmicrosoft.com/B2C_1_SignUpIn",
redirectUri: "http://localhost:3000",
validateAuthority: false,
postLogoutRedirectUri: "http://localhost:3000/home",
knownAuthorities: ["https://tenantB.b2clogin.com/tenantB.onmicrosoft.com"],
scopes: ["https://tenantA.onmicrosoft.com/api/xxx/Core.API.All"],
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
}
}
I have user flow under ADB2C where react app is registered tenant B
Web API Security
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;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
//var azureAdB2CConfig = builder.Configuration.GetSection("AzureAdB2C");
options.Audience = azureAdB2CConfig["ClientId"];
options.Authority = $"{azureAdB2CConfig["Instance"]}/{tenantName}.onmicrosoft.com/{signUpInPolicy}/v2.0/";
// Explicitly set the valid issuer
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
//ValidIssuer = $"{azureAdB2CConfig["Instance"]}/v2.0/",
ValidIssuers = new[]
{
$"{azureAdB2CConfig["Instance"]}/v2.0/",
$"https://TenantB.b2clogin.com/xxx/v2.0/"
},
ValidateIssuerSigningKey = true,
// Ensure the token audience matches the configured audience
ValidateAudience = true,
ValidAudience = azureAdB2CConfig["ClientId"],
// Optionally, validate the lifetime of the token
ValidateLifetime = true,
};
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
}
}