Check User Authorization with Aspect Oriented Programming in Asp.Net Core 3.1 Web Api - asp.net-core

I want to check user authorization in the api method.
Method responsible for get for an employee by id. So user should render this method if the user works the same company with employee. So I mean user CompanyId should be same with the Employee CompanyId.
Think about like this api method:
public async Task<IActionResult> GetEmployeeById([FromRoute]int id)
{
try
{
var entity = await _employeeRepository.GetAsync(p => p.Id == id);
if (entity == null)
{
return NotFound();
}
//user's CompanyId should be same with Employee CompanyId
var user = await _userManager.FindByIdAsync(User.Identity.Name);
if (user.CompanyId != eployee.CompanyId)
{
return Unauthorized();
}
return Ok(entity);
}
catch (Exception ex)
{
throw ex;
}
}
I have to check user company in all api methods.
So I want to do this by Aspect Oriented Programming.So method should be like this after AOP implementation:
[CheckUserCompany]
public async Task<IActionResult> GetEmployeeById([FromRoute]int id)
{
try
{
var entity = await _employeeRepository.GetAsync(p => p.Id == id);
if (entity == null)
{
return NotFound();
}
return Ok(entity);
}
catch (Exception ex)
{
throw ex;
}
}
How can I do this with Aspect Oriented Programming in Asp.Net Core 3.1 Web Api?
Thanks

You could customize a ActionFilter like below:
public class CheckUserCompany:IActionFilter
{
private readonly ApplicationDbContext _dbcontext;
private readonly UserManager<ApplicationUser> _userManager;
public CheckUserCompany(ApplicationDbContext dbcontext, UserManager<ApplicationUser> userManager)
{
_dbcontext = dbcontext;
_userManager = userManager;
}
// Do something before the action executes.
public void OnActionExecuting(ActionExecutingContext context)
{
var id = (int)context.ActionArguments["id"];
var employee = _dbcontext.Employee.Find(id);
var user = _userManager.FindByNameAsync(context.HttpContext.User.Identity.Name);
if (user.Result.CompanyId != employee.CompanyId)
{
throw new UnauthorizedAccessException();
}
return;
}
// Do something after the action executes.
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
If we want to use our filter as a service type on the Action or Controller level, we need to register it in the same ConfigureServices method but as a service in the IoC container:
services.AddScoped<CheckUserCompany>();
Finally, to use a filter registered on the Action or Controller level, we need to place it on top of the Controller or Action as a ServiceType:
[ServiceFilter(typeof(CheckUserCompany))]
public async Task<IActionResult> GetEmployeeById([FromRoute]int id)

Related

How to handle authorization error after migrated from ASP.NET Core 2.1 to .NET 6?

I have migrated my project from asp.net core 2.1 to .NET 6, and now I am facing an error with
context.Resource as AuthorizationFilterContext which is return NULL.
I have implemented a custom Policy-based Authentication using AuthorizationFilterContext, It seems that.NET 6 do not support AuthorizationFilterContext Please help me how to modify the below code from asp.net core 2.1 to .NET6. thank you.
Here is the error message in this line var mvcContext = context.Resource as AuthorizationFilterContext;
mvcContext == NULL
Here is the Implemention Code of AuthorizationHandler and AuthorizationHandlerContext
public class HasAccessRequirment : IAuthorizationRequirement { }
public class HasAccessHandler : AuthorizationHandler<HasAccessRequirment>
{
public readonly HoshmandDBContext _context;
public HasAccessHandler(HoshmandDBContext context)
{
_context = context;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasAccessRequirment requirement)
{
Contract.Ensures(Contract.Result<Task>() != null);
List<int?> userGroupIds = new List<int?>();
// receive the function informations
var mvcContext = context.Resource as AuthorizationFilterContext;
if ((mvcContext != null) && !context.User.Identity.IsAuthenticated)
{
mvcContext.Result = new RedirectToActionResult("UserLogin", "Logins", null);
return Task.FromResult(Type.Missing);
}
if (!(mvcContext?.ActionDescriptor is ControllerActionDescriptor descriptor))
{
return Task.FromResult(Type.Missing);
}
var currntActionAddress = descriptor.ControllerName + "/" + descriptor.ActionName;
// finding all information about controller and method from Tables
// check user has access to current action which is being called
//allActionInfo = ListAcctionsFromDatabase;
//bool isPostBack = allActionInfo.FirstOrDefault(a => a.action == currntActionAddress)?.IsMenu ?? true;
bool isPostBack = false;
if (!isPostBack)
{
mvcContext.Result = new RedirectToActionResult("AccessDenied", descriptor.ControllerName, null);
context.Succeed(requirement);
return Task.CompletedTask;
}
else
{
mvcContext.Result = new RedirectToActionResult("AccessDeniedView", descriptor.ControllerName, null);
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
Here is my Program.cs Code:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("HasAccess", policy => policy.AddRequirements(new HasAccessRequirment()));
});
builder.Services.AddTransient<IAuthorizationHandler, HasAccessHandler>();
Here is the Controller Code:
[Authorize(policy: "HasAccess")]
public class HomeController : BaseController
{
}
There are some changes since .net core 3 about AuthorizationFilterContext:
A. MVC is no longer add AuthorizeFilter to ActionDescriptor and ResourceInvoker won’t call AuthorizeAsync().
B. It will add Filter as metadata to endpoint. Also, in .net 5 it changed the context.Resource as the type of DefaultHttpContext.
So here is the new method:
public class MyAuthorizationPolicyHandler : AuthorizationHandler<OperationAuthorizationRequirement>
{
public MyAuthorizationPolicyHandler()
{
}
protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
{
var result = false;
if (context.Resource is Microsoft.AspNetCore.Http.DefaultHttpContext httpContext)
{
var endPoint = httpContext.GetEndpoint();
if (endPoint != null)
{
var attributeClaims = endPoint.Metadata.OfType<MyAuthorizeAttribute>()
//TODO: Add your logic here
}
if (result)
{
context.Succeed(requirement);
}
}
}
Please refer to this discussion: "context.Resource as AuthorizationFilterContext" returning null in ASP.NET Core 3.0

How to access ModelState in UseExceptionHandler

I have domain service that throws a custom DomainServiceValidationException for a business validation. I want to globally capture the exception and return in ModelState. My current working solution is using ExceptionFilterAttribute
public class ExceptionHandlerAttribute : ExceptionFilterAttribute
{
private readonly ILogger<ExceptionHandlerAttribute> _logger;
public ExceptionHandlerAttribute(ILogger<ExceptionHandlerAttribute> logger)
{
_logger = logger;
}
public override void OnException(ExceptionContext context)
{
if (context == null || context.ExceptionHandled)
{
return;
}
if(context.Exception is DomainServiceValidationException)
{
context.ModelState.AddModelError("Errors", context.Exception.Message);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Result = new BadRequestObjectResult(context.ModelState);
}
else
{
handle exception
}
}
}
I want to know if there is any way to access ModelState in UseExceptionHandler middleware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
{
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
}
});
}
}
Shorten answer:
No, you cannot access ModelState in UseExceptionHandler middleware.
Explanation:
Firstly you need to know ModelState is only available after Model Binding.
Then Model Binding invokes before Action Filters and after Resource Filters(See Figure 1). But Middleware invokes before Filters(See Figure 2).
Figure 1:
Figure 2:
Reference:
How Filters work
Conclusion:
That is to say, you can not get the ModelState in UseExceptionHandler middleware.
Workaround:
You can only store the ModelState automatically in Filters(Action Filters or Exception Filters or Result Filters), then you can use it within middleware.
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
var data = context.Features.Get<ModelStateFeature>()?.ModelState;
}
});
Reference:
How to store ModelState automatically

Unable to seed data in ASP.NET Core in a static method due to exception 'A second operation started on this context before a previous'

I am attempting to seed my database with the following code:
Startup.Configure:
app.UseCors("AllowAll")
.UseMiddleware<JwtBearerMiddleware>()
.UseAuthentication()
.SeedDatabase() <= here
.UseHttpsRedirection()
.UseDefaultFiles()
.UseMvc()
.UseSpa(SpaApplicationBuilderExtensions => { });
SeedDatabase method:
public static IApplicationBuilder SeedDatabase(this IApplicationBuilder app)
{
IServiceProvider serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
try
{
UserManager<ApplicationUser> userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();
RoleManager<IdentityRole> roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
IConfiguration configuration = serviceProvider.GetService<IConfiguration>();
ThePLeagueContext dbContext = serviceProvider.GetService<ThePLeagueContext>();
DataBaseInitializer.SeedUsers(userManager, roleManager, configuration, dbContext);
DataBaseInitializer.SeedTeams(dbContext);
}
catch (Exception ex)
{
ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
return app;
}
Everything worked fine until I added ThePLeagueContext dbContext = serviceProvider.GetService<ThePLeagueContext>(); and then the DataBaseInitializer.SeedTeams(dbContext)
DataBaseInitializer.SeedTeams(dbContext):
public static async void SeedTeams(ThePLeagueContext dbContext)
{
List<Team> teams = new List<Team>();
// 7 because we have 7 leagues
for (int i = 0; i < 7; i++)...
if (dbContext.Teams.Count() < teams.Count)
{
foreach (Team newTeam in teams)
{
await dbContext.Teams.AddAsync(newTeam);
await dbContext.SaveChangesAsync();
}
}
}
When I attempt to seed the database with the above code I get the following exception:
System.InvalidOperationException: 'A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe. This could also be caused by a nested query being evaluated on the client, if this is the case rewrite the query avoiding nested invocations.'
My database context is registered with the LifeTime of Scoped.
Two workarounds I found:
When I change my database context to Transient the seeding issue goes away. This however causes other issues in the application so I cannot use Transient
When I call DatabaseInitializer.SeedTeams(dbContext) from inside the DatabaseInitializer.SeedUsers(...) method, this also works, I have no clue why.
DatabaseInitializer.SeedUsers(...) method:
public async static void SeedUsers(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, ThePLeagueContext dbContext)
{
string[] roles = new string[] { AdminRole, SuperUserRole, UserRole };
foreach (string role in roles)
{
if (!roleManager.Roles.Any(r => r.Name == role))
{
IdentityRole newRole = new IdentityRole
{
Name = role,
NormalizedName = role.ToUpper()
};
await roleManager.CreateAsync(newRole);
if (role == AdminRole)
{
await roleManager.AddClaimAsync(newRole, new Claim(Permission, ModifyPermission));
}
else if (role == SuperUserRole)
{
await roleManager.AddClaimAsync(newRole, new Claim(Permission, RetrievePermission));
}
else
{
await roleManager.AddClaimAsync(newRole, new Claim(Permission, ViewPermission));
}
}
}
ApplicationUser admin = new ApplicationUser()...
ApplicationUser sysAdmin = new ApplicationUser()...;
PasswordHasher<ApplicationUser> password = new PasswordHasher<ApplicationUser>();
if (!userManager.Users.Any(u => u.UserName == admin.UserName))
{
string hashed = password.HashPassword(admin, configuration["ThePLeagueAdminInitPassword"]);
admin.PasswordHash = hashed;
await userManager.CreateAsync(admin);
await userManager.AddToRoleAsync(admin, AdminRole);
}
if (!userManager.Users.Any(u => u.UserName == sysAdmin.UserName))
{
string hashed = password.HashPassword(sysAdmin, configuration["ThePLeagueAdminInitPassword"]);
sysAdmin.PasswordHash = hashed;
await userManager.CreateAsync(sysAdmin);
await userManager.AddToRoleAsync(sysAdmin, AdminRole);
}
SeedTeams(dbContext);
}
Is there any way I can use two separate static async methods to seed the database and keep my context as scoped?
So I like to keep things ordered and seperated. Therefore I'd do something like:
public static class SeedData
{
public static void Populate(IServiceProvider services)
{
ApplicationDbContext context = services.GetRequiredService<ApplicationDbContext>();
if (!context.SomeDbSet.Any())
{
// ...code omitted for brevity...
);
context.SaveChanges();
}
}
public static class IdentitySeedData
{
public static async Task Populate(IServiceProvider services)
{
UserManager<ApplicationUser> userManager = services.GetService<UserManager<ApplicationUser>>();
RoleManager<IdentityRole> roleManager = services.GetService<RoleManager<IdentityRole>>();
IConfiguration configuration = services.GetService<IConfiguration>();
ApplicationDbContext context = services.GetRequiredService<ApplicationDbContext>();
if (!context.Users.Any())
{
// ...code omitted for brevity...
await userManager.CreateAsync(sysAdmin);
await userManager.AddToRoleAsync(sysAdmin, AdminRole);
);
context.SaveChanges();
}
}
And then the one to top it off:
public static class DatabaseInitializer
{
public static void Initialize(IServiceProvider services)
{
IdentitySeedData.Populate(services).Wait();
SeedData.Populate(services);
}
}
Disclaimer: I haven't run the code. So if it requires some tweaking let me know. I'll make the adjustments. It's a bit time-consuming to test this out.

How do I get the current logged in user ID in the ApplicationDbContext using Identity?

I have created a .net core 2.1 MVC application using the template in Visual Studio with the Identity preset (user accounts stored in the application) and I am trying to automate some auditing fields.
Basically what I'm trying to do is overriding the SaveChangesAsync() method so that whenever changes are made to an entity the current logged in user ID is set to the auditing property of CreatedBy or ModifiedBy properties that are created as shadow properties on the entity.
I have looked at what seems to be tons of answers and surprisingly none of them work for me. I have tried injecting IHttpContext, HttpContext, UserManager, and I either can't seem to access a method that returns the user ID or I get a circular dependency error which I don't quite understand why it is happening.
I'm really running desperate with this one. I think something like this should be really straightforward to do, but I'm having a real hard time figuring out how to do it. There seem to be well documented solutions for web api controllers or for MVC controllers but not for use inside the ApplicationDbContext.
If someone can help me or at least point me into the right direction I'd be really grateful, thanks.
Let's call it DbContextWithUserAuditing
public class DBContextWithUserAuditing : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public string UserId { get; set; }
public int? TenantId { get; set; }
public DBContextWithUserAuditing(DbContextOptions<DBContextWithUserAuditing> options) : base(options) { }
// here we declare our db sets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.NamesToSnakeCase(); // PostgreSQL
modelBuilder.EnableSoftDelete();
}
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
ChangeTracker.ProcessModification(UserId);
ChangeTracker.ProcessDeletion(UserId);
ChangeTracker.ProcessCreation(UserId, TenantId);
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
ChangeTracker.DetectChanges();
ChangeTracker.ProcessModification(UserId);
ChangeTracker.ProcessDeletion(UserId);
ChangeTracker.ProcessCreation(UserId, TenantId);
return (await base.SaveChangesAsync(true, cancellationToken));
}
}
Then you have request pipeline and what you need - is a filter hook where you set your UserID
public class AppInitializerFilter : IAsyncActionFilter
{
private DBContextWithUserAuditing _dbContext;
public AppInitializerFilter(
DBContextWithUserAuditing dbContext
)
{
_dbContext = dbContext;
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next
)
{
string userId = null;
int? tenantId = null;
var claimsIdentity = (ClaimsIdentity)context.HttpContext.User.Identity;
var userIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
userId = userIdClaim.Value;
}
var tenantIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == CustomClaims.TenantId);
if (tenantIdClaim != null)
{
tenantId = !string.IsNullOrEmpty(tenantIdClaim.Value) ? int.Parse(tenantIdClaim.Value) : (int?)null;
}
_dbContext.UserId = userId;
_dbContext.TenantId = tenantId;
var resultContext = await next();
}
}
You activate this filter in the following way (Startup.cs file)
services
.AddMvc(options =>
{
options.Filters.Add(typeof(OnRequestInit));
})
Your app is then able to automatically set UserID & TenantID to newly created records
public static class ChangeTrackerExtensions
{
public static void ProcessCreation(this ChangeTracker changeTracker, string userId, int? tenantId)
{
foreach (var item in changeTracker.Entries<IHasCreationTime>().Where(e => e.State == EntityState.Added))
{
item.Entity.CreationTime = DateTime.Now;
}
foreach (var item in changeTracker.Entries<IHasCreatorUserId>().Where(e => e.State == EntityState.Added))
{
item.Entity.CreatorUserId = userId;
}
foreach (var item in changeTracker.Entries<IMustHaveTenant>().Where(e => e.State == EntityState.Added))
{
if (tenantId.HasValue)
{
item.Entity.TenantId = tenantId.Value;
}
}
}
I wouldn't recommend injecting HttpContext, UserManager or anything into your DbContext class because this way you violate Single Responsibility Principle.
Thanks to all the answers. In the end I decided to create a UserResolveService that receives through DI the HttpContextAccessor and can then get the current user's name. With the name I can then query the database to get whatever information I may need. I then inject this service on the ApplicationDbContext.
IUserResolveService.cs
public interface IUserResolveService
{
Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext);
}
UserResolveService.cs
public class UserResolveService : IUserResolveService
{
private readonly IHttpContextAccessor httpContextAccessor;
public UserResolveService(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public async Task<string> GetCurrentSessionUserId(IdentityDbContext dbContext)
{
var currentSessionUserEmail = httpContextAccessor.HttpContext.User.Identity.Name;
var user = await dbContext.Users
.SingleAsync(u => u.Email.Equals(currentSessionUserEmail));
return user.Id;
}
}
You have to register the service on startup and inject it on the ApplicationDbContext and you can use it like this:
ApplicationDbContext.cs
var dbContext = this;
var currentSessionUserId = await userResolveService.GetCurrentSessionUserId(dbContext);

.Net Core 2.0 Web API controller not working and getting 404

I have something very very strange. I have 2 controllers. UploadController and AccountController. Theye were both working, and now when I try the AccountController it give error 404. ik don't get it.
This is how my AccountController looks like:
namespace CoreAngular.Controllers
{
//[Authorize]
[Produces("application/json")]
[Route("api/account")]
public class AccountController : Controller
{
private IRepository repository;
public AccountController(IDatabaseClient<DocumentClient> client)
: this ( new UserRepository(client))
{
}
public AccountController(IRepository repository)
{
this.repository = repository;
}
[HttpGet]
public async Task<ActionResult> Get(string id)
{
var start = DateTime.Now.TimeOfDay;
if (string.IsNullOrEmpty(id))
{
return BadRequest();
}
var user = await repository.GetAsync(id);
if (user == null)
{
return NotFound();
}
var userDTO = new UserGetDTO()
{
image = Convert.ToBase64String(user.image.image),
id = user.id,
time = DateTime.Now.Subtract(start).Millisecond
};
return Ok(userDTO);
}......
Do I miss something here? I know I comentet out the [Authorize], but i just wanted to try to connect.
You should specify route template in HttpGet attribute:
[HttpGet("{id}")]
public async Task<ActionResult> Get(string id)
{
// ...
}