I am developing an ASP.NET Core MVC application that uses Microsoft Entra ID for authentication. The application is designed to support multiple tenants, and I want to restrict access so that only users from specific tenants can log in. I have configured an AllowedTenants
array in my appsettings.json
file and am attempting to block users from tenants not included in this array. Despite this, my current implementation is not functioning as intended, and users from unauthorized tenants are still able to access the application.
Here is my setup:
Program.cs
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var sqlConnectionString = builder.Configuration.GetConnectionString("DbContext");
builder.Services.AddDbContext<MyAppContext>(options =>
options.UseSqlServer(sqlConnectionString ?? throw new InvalidOperationException("Connection string not found.")));
var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants").Get<string[]>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetSection("DownstreamApis:MicrosoftGraph:Scopes").Get<string[]>())
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApis:MicrosoftGraph"))
.AddInMemoryTokenCaches();
builder.Services.Configure<JwtBearerOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
var tenantId = context.Principal?.FindFirst("tid")?.Value;
if (!allowedTenants!.Contains(tenantId))
{
throw new UnauthorizedAccessException("This tenant is not authorized");
}
};
});
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages(options =>
{
})
.AddMvcOptions(options => { })
.AddMicrosoftIdentityUI();
builder.Services.AddScoped<IFormFileCheckerInterface, IFormFileChecker>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseRewriter(
new RewriteOptions().Add(
context =>
{
if (context.HttpContext.Request.Path == "/MicrosoftIdentity/Account/SignedOut")
{ context.HttpContext.Response.Redirect("/Home/Index"); }
})
);
app.Run();
}
}
appsettings.json
{
"AzureAd": {
"Instance": "micrososft_instance",
"Domain": "my_domain",
"ClientId": "client_id",
"ClientSecret": "client_secret",
"TenantId": "organizations",
"CallbackPath": "/signin-oidc",
"AllowedTenants": [ "tenant_one", "tenant_two" ]
},
"DownstreamApis": {
"MicrosoftGraph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": [ "User.Read", "User.ReadWrite.All" ]
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DbContext": "db_context"
},
"AllowedHosts": "*"
}
Controllerfunction:
[HttpGet]
[Authorize]
public IActionResult Index()
{
try
{
return View();
}
catch (Exception ex)
{
_logger.LogError("An error occurred when trying to return Index page: " + ex.Message);
return StatusCode(500, "An error occurred when trying to return Index page");
}
}
What I am expecting:
I expected the application to throw an UnauthorizedAccessException
and deny access to users from tenants not listed in the AllowedTenants
array. However, users from unauthorized tenants are still able to access the application.
What could be wrong in my implementation, and how can I correctly restrict access based on tenant ID?