Security Cosplay: Your Password-Only Admin Panel Isn’t Fooling Anyone
Username and password authentication died years ago. Yet here we are in 2026, still finding production systems where administrative operations require nothing more than knowing someone’s password. That’s not security. That’s security cosplay: you’ve got the costume, the role-based authorization attributes, maybe even a fancy login page. But underneath? Nothing that would stop an attacker with a leaked credential from walking straight into your admin panel.
If you’re building customer-facing applications, especially those handling sensitive data, your authentication architecture needs multi-factor authentication for privileged operations. Not as a checkbox for auditors, but as actual protection.
Azure AD B2C provides enterprise-grade identity management with conditional MFA capabilities, but implementing it correctly requires understanding the platform’s custom policy framework. This isn’t about blindly enabling MFA everywhere. It’s about risk-based authentication that balances security with user experience while maintaining audit trails for compliance.
What Secure Authentication Actually Requires
Before diving into implementation, let’s examine what proper authentication demands for enterprise systems.
Secure log-on procedures specify that access to systems and applications shall be controlled appropriately. This includes multi-factor authentication for privileged operations, limiting login attempts, enforcing timeout periods, and maintaining audit logs of authentication events. Compliance frameworks like ISO 27001 (Control A.9.4.2) codify these requirements, but they’re fundamentally about sound security practice.
Password management addresses password quality requirements. Modern standards mandate enforcing password complexity, preventing password reuse, requiring secure password storage (hashed and salted), and providing secure password change procedures. Control A.9.4.3 captures these, though the principles predate any specific standard.
Secret authentication information management covers the lifecycle of authentication credentials. This includes secure distribution of initial passwords, requiring password changes on first use, protecting authentication secrets during transmission and storage, and ensuring users acknowledge their responsibility for maintaining confidentiality. ISO’s Control A.9.2.4 formalizes what security engineers have known for decades.
These requirements work together to create a defense-in-depth authentication strategy. MFA addresses the fundamental weakness of password-only authentication: a compromised password alone cannot grant access. Combined with strong password policies and proper secret management, you create an authentication system that withstands real-world attack scenarios. Whether you’re pursuing formal certification or simply building secure systems, these principles apply equally.
The Fatal Example: Security Cosplay
Let me show you what not to do. This example represents a common pattern I’ve seen in production systems. Authentication that looks secure at first glance but fails under scrutiny:
// Fatal authentication configuration
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30); // Extended session
options.SlidingExpiration = true;
});
services.Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 6; // Weak minimum
options.Password.RequireDigit = false;
options.Password.RequireNonAlphanumeric = false;
});
// Password-only login with no lockout
var result = await _signInManager.PasswordSignInAsync(
model.Username, model.Password,
isPersistent: true, lockoutOnFailure: false);
// Critical operation with only role check
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DeleteAllCustomerData(int customerId)
{
await _customerService.DeleteCustomerDataAsync(customerId);
return Ok();
}
This code violates multiple security controls simultaneously.
No multi-factor authentication: Administrative users authenticate with only username and password. A compromised password grants full access to critical operations. One phishing email, one credential dump, one reused password from a breached service, and attackers own your admin panel.
Weak password policies: Six-character passwords without complexity requirements fail basic security standards. Modern password cracking tools can brute-force these in seconds on commodity hardware.
No account lockout: Failed login attempts don’t trigger account lockout, enabling unlimited brute-force attempts. Attackers can systematically try millions of passwords without any friction.
Extended session lifetime: 30-day cookies with sliding expiration create persistent access tokens. If a session token gets compromised through XSS or session hijacking, attackers maintain access for a month.
Missing audit trails: No logging of authentication events means you can’t detect or investigate security incidents. When something goes wrong, and it will, you’ll have no forensic evidence.
Insecure password reset: Reset tokens in email URLs can be leaked through browser history, email forwarding, or server logs. No MFA verification during reset means password reset becomes a backdoor.
No risk-based authentication: Critical operations like deleting customer data require the same authentication as viewing a dashboard. No step-up authentication for high-risk actions means privilege escalation is trivially easy.
This isn’t just bad practice. It’s an audit failure waiting to happen. When your security reviewer or compliance auditor examines this code, you’ll be documenting remediation plans before anything else proceeds.
The Correct Implementation: Azure AD B2C with Conditional MFA
Now let’s implement proper authentication using Azure AD B2C with custom policies for risk-based MFA. This approach separates authentication concerns from your application code while maintaining full control over the authentication flow. The key insight: authentication complexity belongs in your identity provider, not scattered throughout your application.
Configuring Azure AD B2C Custom Policies
Custom policies in Azure AD B2C use an XML-based framework called the Identity Experience Framework (IEF). While the XML is verbose, it provides granular control over every step of the authentication process. Here’s a condensed policy that enforces conditional MFA based on operation risk:
<!-- TrustFrameworkExtensions.xml - Conditional MFA Policy -->
<TrustFrameworkPolicy xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
TenantId="yourtenant.onmicrosoft.com"
PolicyId="B2C_1A_TrustFrameworkExtensions">
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="operationRiskLevel">
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="mfaCompleted">
<DataType>boolean</DataType>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
<ClaimsProviders>
<ClaimsProvider>
<TechnicalProfiles>
<TechnicalProfile Id="AzureMfa-Input">
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.AzureMfaProtocolProvider" />
<Metadata>
<Item Key="Operation">Verify</Item>
<Item Key="AuthenticationMethodsAllowed">Phone, Email, Authenticator</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="mfaCompleted" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignUpOrSignInWithConditionalMfa">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" />
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>operationRiskLevel</Value>
<Value>high</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AzureMfaExchange"
TechnicalProfileReferenceId="AzureMfa-Input" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="SendClaims"
CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
</UserJourney>
</UserJourneys>
</TrustFrameworkPolicy>
The policy defines a user journey with conditional MFA. Step 2 only executes when operationRiskLevel equals “high”. Your application passes this claim when initiating authentication for privileged operations. Low-risk operations skip MFA entirely, providing a smooth user experience for routine tasks while enforcing strong authentication where it matters.
ASP.NET Core Integration
Now integrate this into your ASP.NET Core application. The Microsoft.Identity.Web library handles the heavy lifting:
// Program.cs - ASP.NET Core 8+ with Azure AD B2C
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
options.SignUpSignInPolicyId = "B2C_1A_SignUpOrSignInWithConditionalMfa";
options.UseTokenLifetime = true;
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
var userId = context.Principal?.FindFirst("sub")?.Value;
var mfaCompleted = context.Principal?
.FindFirst("mfaCompleted")?.Value == "true";
logger.LogInformation(
"User {UserId} authenticated. MFA: {MfaCompleted}",
userId, mfaCompleted);
return Task.CompletedTask;
}
};
});
// Authorization policies with risk-based MFA requirements
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadAccess", policy =>
policy.RequireAuthenticatedUser());
options.AddPolicy("AdminOperations", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("mfaCompleted", "true");
policy.RequireRole("Administrator");
});
options.AddPolicy("CriticalOperations", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("mfaCompleted", "true");
policy.RequireAssertion(context =>
{
var mfaTimestamp = context.User.FindFirst("lastMfaTimestamp")?.Value;
if (long.TryParse(mfaTimestamp, out var timestamp))
{
var mfaTime = DateTimeOffset.FromUnixTimeSeconds(timestamp);
return (DateTimeOffset.UtcNow - mfaTime).TotalMinutes <= 5;
}
return false;
});
});
});
builder.Services.AddHealthChecks()
.AddCheck<AzureAdB2CHealthCheck>("azureadb2c");
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
Three authorization policies emerge from this configuration. ReadAccess requires only authentication for low-risk operations like viewing dashboards. AdminOperations requires both authentication and completed MFA for administrative tasks. CriticalOperations goes further: it requires MFA completion within the last 5 minutes. This step-up authentication pattern ensures that even if an attacker hijacks an authenticated session, they can’t perform destructive operations without fresh MFA verification.
Controller Implementation with Risk-Based Authorization
Implement controllers with proper authorization attributes. Notice how different operations require different security levels:
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
private readonly ILogger<AdminController> _logger;
private readonly ICustomerService _customerService;
public AdminController(ILogger<AdminController> logger,
ICustomerService customerService)
{
_logger = logger;
_customerService = customerService;
}
[HttpGet("dashboard")]
[Authorize(Policy = "ReadAccess")]
public IActionResult GetDashboard()
{
_logger.LogInformation("User {UserId} accessed dashboard",
User.FindFirst("sub")?.Value);
return Ok(new { message = "Dashboard data" });
}
[HttpPost("users/{userId}/disable")]
[Authorize(Policy = "AdminOperations")]
public async Task<IActionResult> DisableUser(string userId)
{
_logger.LogWarning("Admin {AdminId} disabled user {UserId}",
User.FindFirst("sub")?.Value, userId);
await _customerService.DisableUserAsync(userId);
return Ok();
}
[HttpDelete("customers/{customerId}/data")]
[Authorize(Policy = "CriticalOperations")]
public async Task<IActionResult> DeleteCustomerData(int customerId)
{
_logger.LogCritical("Admin {AdminId} deleted data for {CustomerId}",
User.FindFirst("sub")?.Value, customerId);
await _customerService.DeleteCustomerDataAsync(customerId);
return NoContent();
}
}
The pattern becomes clear. Dashboard access needs only authentication. Disabling a user requires MFA. Deleting customer data requires MFA within the last 5 minutes. Each operation matches its authorization policy to the actual risk it represents.
MFA Enrollment Monitoring Dashboard
You can’t secure what you can’t measure. Create an admin endpoint to monitor MFA enrollment across your user base:
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "AdminOperations")]
public class MfaMonitoringController : ControllerBase
{
private readonly GraphServiceClient _graphClient;
private readonly ILogger<MfaMonitoringController> _logger;
public MfaMonitoringController(GraphServiceClient graphClient,
ILogger<MfaMonitoringController> logger)
{
_graphClient = graphClient;
_logger = logger;
}
[HttpGet("enrollment-stats")]
public async Task<IActionResult> GetMfaEnrollmentStats()
{
var users = await _graphClient.Users.GetAsync(config =>
{
config.QueryParameters.Select = new[] { "id", "displayName" };
config.QueryParameters.Top = 999;
});
var totalUsers = users?.Value?.Count ?? 0;
var mfaEnrolledUsers = 0;
foreach (var user in users?.Value ?? [])
{
var authMethods = await _graphClient.Users[user.Id]
.Authentication.Methods.GetAsync();
if (authMethods?.Value?.Any() == true)
mfaEnrolledUsers++;
}
var enrollmentRate = totalUsers > 0
? (double)mfaEnrolledUsers / totalUsers * 100 : 0;
return Ok(new
{
TotalUsers = totalUsers,
MfaEnrolledUsers = mfaEnrolledUsers,
EnrollmentRate = Math.Round(enrollmentRate, 2),
ComplianceStatus = enrollmentRate >= 95 ? "Compliant" : "Non-Compliant"
});
}
}
This endpoint uses Microsoft Graph to query user authentication methods. For administrative users, target 100% MFA enrollment. For regular users, 95% is a reasonable threshold. The compliance status gives you immediate visibility into whether your organization meets enrollment requirements.
Health Checks for Identity Service
Your application depends on Azure AD B2C being available. Implement health checks to detect identity service problems before they impact users:
public class AzureAdB2CHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
public AzureAdB2CHealthCheck(IHttpClientFactory httpClientFactory,
IConfiguration configuration)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
var instance = _configuration["AzureAdB2C:Instance"];
var domain = _configuration["AzureAdB2C:Domain"];
var policy = _configuration["AzureAdB2C:SignUpSignInPolicyId"];
var metadataUrl = $"{instance}/{domain}/{policy}/v2.0/" +
".well-known/openid-configuration";
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(5);
try
{
var response = await client.GetAsync(metadataUrl, ct);
return response.IsSuccessStatusCode
? HealthCheckResult.Healthy("Azure AD B2C is responsive")
: HealthCheckResult.Degraded($"Status: {response.StatusCode}");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Azure AD B2C unreachable", ex);
}
}
}
The health check queries the OpenID Connect metadata endpoint. If Azure AD B2C becomes unavailable or degrades, your application learns about it through standard health check infrastructure rather than through failing user logins.
Practical Recommendations for Production MFA
Implementing MFA correctly requires more than just enabling the feature. Here’s what actually matters when you’re building for real users and real auditors.
Document your authentication policy: Security reviewers and compliance auditors will ask for your documented authentication policy. Specify which operations require MFA, acceptable authentication methods, session timeout values, and password complexity requirements. For formal certifications, reference the specific controls your policy addresses. For internal governance, focus on the rationale behind each decision.
Monitor enrollment rates: Track MFA enrollment across your user base. For administrative users, aim for 100% enrollment. Period. For regular users, target at least 95%. The monitoring dashboard shown above provides the metrics reviewers expect to see. Low enrollment rates signal either poor communication or friction in the enrollment process.
Implement risk-based authentication: Not every operation needs MFA, but privileged operations absolutely do. Use claims-based authorization to enforce step-up authentication for critical operations. The example shows MFA required within 5 minutes for data deletion. Adjust this window based on your risk assessment and operational needs.
Audit authentication events: Log all authentication attempts, both successful and failed. Log MFA challenges and privileged operations. Structure these logs for easy querying during security investigations. Include user ID, timestamp, MFA method used, and operation performed. When something goes wrong, these logs become your forensic evidence.
Support multiple MFA methods: Phone, email, authenticator apps, and FIDO2 security keys each have different security characteristics. Supporting multiple methods improves both security (users can choose stronger methods) and availability (fallback options when primary method unavailable). Don’t force everyone into SMS verification when authenticator apps offer better security.
Test password reset flows: Password reset is a common attack vector. Social engineering attacks often target reset processes. Ensure your reset process requires MFA verification before issuing new credentials. Never send reset tokens in email URLs. Use Azure AD B2C’s built-in secure reset flows instead.
Plan for MFA recovery: Users lose phones and security keys. Document and implement a secure MFA recovery process that doesn’t undermine the security MFA provides. This typically involves identity verification by authorized administrators with full audit logging. The recovery process itself becomes an attack vector if not properly secured.
Configure session timeouts appropriately: Security standards require timeout periods for authenticated sessions. For administrative users, 15-30 minutes is typical. For regular users, balance security with user experience: 2-4 hours is common. Use sliding expiration for regular users but fixed expiration for administrators.
Azure AD B2C custom policies provide the flexibility to implement these requirements while maintaining enterprise-grade security. The platform handles the cryptographic heavy lifting. Your responsibility is configuring policies correctly and integrating them into your authorization model.
Conclusion
Authentication requirements for privileged operations aren’t suggestions. They’re auditable controls that determine whether your organization passes security reviews, achieves compliance certifications, or simply avoids becoming the next breach headline. Multi-factor authentication for privileged operations, strong password management, and proper secret handling work together to create defensible authentication architecture.
Azure AD B2C with conditional MFA policies provides the tools to meet these requirements without building authentication infrastructure from scratch. Custom policies enable risk-based authentication that balances security with user experience. Integration with ASP.NET Core through Microsoft.Identity.Web makes enforcement straightforward. Authentication concerns live in Azure AD B2C. Authorization decisions live in your application code.
The fatal example showed what happens when authentication is an afterthought: weak passwords, no MFA, missing audit trails. These aren’t just bad practices. They’re security failures that block certifications, fail audits, and eventually lead to breaches.
The correct implementation demonstrated conditional MFA based on operation risk, comprehensive audit logging, MFA enrollment monitoring, and health checks for the identity service. This architecture passes both technical security reviews and formal compliance audits.
Start by documenting your authentication policy with specific references to the controls and standards you’re targeting. Implement Azure AD B2C custom policies for conditional MFA. Integrate these policies into your ASP.NET Core authorization model with claims-based policies. Monitor MFA enrollment rates and maintain audit logs for authentication events. When your security reviewer examines your implementation, you’ll demonstrate both technical competence and security awareness. And when attackers probe your admin panel, they’ll find MFA standing between a stolen password and your production data.

Comments