Initial commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PowderCoating.Core.Entities;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Custom ASP.NET Core Identity claims factory that embeds application-specific claims into the
|
||||
/// user's <see cref="ClaimsPrincipal"/> at login time.
|
||||
/// <para>
|
||||
/// Registered in <c>Program.cs</c> via
|
||||
/// <c>builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>()</c>
|
||||
/// so that ASP.NET Core Identity calls it instead of the default factory whenever a cookie is issued.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Why embed claims at login?</b> Reading <c>CompanyId</c> or <c>CompanyRole</c> from the database
|
||||
/// on every request would add a synchronous DB round-trip to the authentication middleware, which runs
|
||||
/// on every single HTTP request. Embedding the values as signed cookie claims at login time means
|
||||
/// subsequent requests are verified cryptographically with zero extra DB queries.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Claims added:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>"CompanyId"</c> — the integer ID of the company the user belongs to. Read by <see cref="Data.ApplicationDbContext.CurrentCompanyId"/> to enforce multi-tenancy query filters.</description></item>
|
||||
/// <item><description><c>"CompanyRole"</c> — the user's role within their company (e.g., CompanyAdmin, Manager, Worker). Distinct from the ASP.NET Identity system role (SuperAdmin).</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialises the factory with the Identity infrastructure services required by the base class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Used by the base class to load user-level claims.</param>
|
||||
/// <param name="roleManager">Used by the base class to load role-level claims.</param>
|
||||
/// <param name="options">Identity options, including the claim-type mappings used by the base class.</param>
|
||||
public CustomUserClaimsPrincipalFactory(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<IdentityRole> roleManager,
|
||||
IOptions<IdentityOptions> options)
|
||||
: base(userManager, roleManager, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the <see cref="ClaimsIdentity"/> for <paramref name="user"/> by first calling the base
|
||||
/// implementation (which adds standard Identity claims such as NameIdentifier, Email, and Role) and
|
||||
/// then appending the application-specific <c>CompanyId</c> and <c>CompanyRole</c> claims.
|
||||
/// <para>
|
||||
/// Each claim addition is wrapped in an individual try/catch to ensure that a missing or malformed
|
||||
/// value on one field does not fail the entire authentication flow — the user is logged in with
|
||||
/// whatever valid claims could be assembled. Errors are written to the debug output rather than
|
||||
/// thrown so that login remains functional even in degraded configurations.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="user">The authenticated <see cref="ApplicationUser"/> whose claims are being constructed.</param>
|
||||
/// <returns>A <see cref="ClaimsIdentity"/> containing all standard and application-specific claims.</returns>
|
||||
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
|
||||
{
|
||||
var identity = await base.GenerateClaimsAsync(user);
|
||||
|
||||
// Add CompanyId claim - only if valid
|
||||
if (user.CompanyId > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
identity.AddClaim(new Claim("CompanyId", user.CompanyId.ToString()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log but don't fail authentication
|
||||
System.Diagnostics.Debug.WriteLine($"Error adding CompanyId claim: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Add CompanyRole claim - only if valid and not empty
|
||||
if (!string.IsNullOrWhiteSpace(user.CompanyRole))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Sanitize the role value to ensure it's safe
|
||||
var sanitizedRole = user.CompanyRole.Trim();
|
||||
identity.AddClaim(new Claim("CompanyRole", sanitizedRole));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log but don't fail authentication
|
||||
System.Diagnostics.Debug.WriteLine($"Error adding CompanyRole claim: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user