I'm working on an ASP.Net Core 3.1 web API.
The API users are from an Azure AD.
The users can access the API if they have a license, each user can be assigned to multiple licenses and the same license can be assigned to multiple users.
The client want me to structure the API routes with a template like <api_url>/{licenseId}/controller/action.
My controllers are like:
[Authorize]
[Route("{licenseId}/Foo")]
public class FooController : ControllerBase
{
If I think of the License as a Resource, I can use the Resource based authorization as detailed here.
It works but I find myself copying and pasting the auth check for all the actions.
Is there a better way to authorize an user using the route values?
Here what I've got so far:
public class LicenseRequirement : IAuthorizationRequirement
{
public Guid LicenseId { get; private set; }
public LicenseRequirement(Guid licenseId)
{
LicenseId = licenseId;
}
}
public class LicenseAuthorizationHandler : AuthorizationHandler<LicenseRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<LicenseAuthorizationHandler> _logger;
private readonly DBContext _db;
public LicenseAuthorizationHandler(DBContext context, ILogger<LicenseAuthorizationHandler> logger, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_db = context;
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, LicenseRequirement requirement)
{
var userId = new Guid(context.User.GetUserId());
var licenseId = _httpContextAccessor.HttpContext.GetRouteData().Values["licenseId"];
if (await _db.ApiUsers.SingleOrDefaultAsync(x => x.LicenseId == new Guid(licenseId as string) && x.UserId == userId) is ApiUser user)
context.Succeed(requirement);
}
}
Now I'm a bit stuck as I don't know how to set it up in Startup.cs and use it as an attribute or area filter creating those requirement at runtime with the licenseId route's value.
I found IAuthorizationFilter pretty straightforward to implement. When I tried it yesterday, I couldn't found the RouteValues but they're there:
public class LicenseAuthorizationFilter : IAuthorizationFilter
{
private readonly ILogger<LicenseAuthorizationFilter> _logger;
private readonly DBContext _db;
public LicenseAuthorizationFilter(DBContext context, ILogger<LicenseAuthorizationFilter> logger)
{
_logger = logger;
_db = context;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var userId = new Guid(context.HttpContext.User.GetUserId());
var licenseId = new Guid(context.HttpContext.Request.RouteValues["licenseId"] as string);
if (!(_db.ApiUsers.SingleOrDefault(x => x.LicenseId == licenseId && x.UserId == userId) is ApiUser user))
{
context.Result = new ForbidResult();
}
}
}
public class LicenseAuthorizationAttribute : TypeFilterAttribute
{
public LicenseAuthorizationAttribute() : base(typeof(LicenseAuthorizationFilter))
{ }
}
And the controller can neatly become:
[Authorize]
[LicenseAuthorization]
[Route("{licenseId}/Items")]
public class ItemsController : ControllerBase
{
[...]
}
Related
public class GroupsController : ControllerBase
{
private readonly ILogger<GroupsController> _logger;
public GroupsController(ILogger<GroupsController> logger)
{
_logger = logger;
string auth = Request.Headers["authorization"];
if (auth is null)
throw new Exception("Missing auth token");
}
[HttpGet("/[controller]/allGroups")]
public List<Group> GetGroups()
{
DbContext dbContext = new DbContext();
List<Group> groups = dbContext.Groups.ToList();
return groups;
}
}
I'm looking to require a authorization header only for this controller, but Request is not possible on the constructor and I don't want to add a auth check on every method on the controller.
Is there a way to check this header on all routes on this controller?
You can carate a custom attribute that validates headers and put it on your action or controller that you want to validate headers. like this:
public class RequiredHeaderAttribute : Attribute, IActionFilter
{
private readonly string _requiredHeader;
public RequiredHeaderAttribute(string requiredHeader)
{
_requiredHeader = requiredHeader;
}
public void OnActionExecuted(ActionExecutedContext context)
{
//
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.HttpContext.Request.Headers.
TryGetValue(_requiredHeader, out _))
{
throw new Exception($"Missing Header Exception: {_requiredHeader}");
}
}
}
Usage:
[RequiredHeader("HeaderName")] //<==NOTE THIS
[HttpGet("/[controller]/allGroups")]
public List<Group> GetGroups()
{
DbContext dbContext = new DbContext();
List<Group> groups = dbContext.Groups.ToList();
return groups;
}
Or
[RequiredHeader("HeaderName")]
public class GroupsController : ControllerBase
{
}
Another way is register RequiredHeader as global filters.
I have a question related to the use of database contexts outside the controller, namely, how to call the database context in a regular class?
To communicate with the database, I use: EF Core
I used options like this:
private readonly MSSQLContext _context;
public BookingController(MSSQLContext context)
{
_context = context;
}
Alternative
using (MSSQLContext context=new MSSQLContext())
{
context.get_Users.ToList();
}
Startup.cs
services.AddDbContext<MSSQLContext>(options =>
options.UseSqlServer(connection));
MSSQLContext.cs
public MSSQLContext()
{
}
public MSSQLContext(DbContextOptions<MSSQLContext> options)
: base(options)
{
}
public DbSet<VIVT_Core_Aud.Models.Core.Logger_Model> Logger_Models { get; set; }
and more tables...
Inject the context into whatever class you need to call into and register that class with the DI framework in your startup class.
For instance,
services.AddTransient<YourType>();
class YourType
{
public YourType(YourDbContext context) { ... }
}
You need to inject context using DI(dependency injection). I am showing you an example of repository pattern. You can search for "Repository pattern EF Core C# examples" will give you lots of examples or blogs to follow. Check here and here
Check out below example..
MyRepository.cs
namespace MyApp.Data.Services
{
public class MyRepository: IMyRepository, IDisposable
{
private MyAppContext _context;
private readonly ILogger<MyRepository> _logger;
public MyRepository(MyAppContext context, ILogger<MyRepository> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger;
}
public IEnumerable<MyTable> GetMyTableData()
{
return _context.MyTable.ToList();
}
}
}
IMyRepository.cs
namespace MyApp.Data.Services
{
public interface IMyRepository
{
//Interface implementation
IEnumerable<MyTable> GetMyTableData();
}
}
Startup.cs of MVC project
services.AddDbContext<MyAppContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("AppDbConnString")));
//Scope of repository should be based on your project used, I recommend to check lifetime & scope based your project needs.
services.AddScoped<IMyRepository, MyRepository>();
Mycontroller.cs
using MyApp.Data.Services;
namespace MyApp.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IMyRepository _myRepository;
public HomeController(ILogger<HomeController> logger, IMyRepository myRepository)
{
_logger = logger;
_myRepository = myRepository ??
throw new ArgumentNullException(nameof(myRepository));
}
public IActionResult Index()
{
return View();
}
public IActionResult GetAllData()
{
var result = _myRepository.GetMyTableData();
return Json(result);
}
}
}
I have a working ActionFilter which sets a ViewBag for use in my MVC views:
public class ViewBagFilter : IActionFilter
{
private readonly ProjectsDbContext db;
public ViewBagFilter(ProjectsDbContext _dbContext)
{
db = _dbContext;
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var controller = context.Controller as Controller;
if (Guid.TryParse(controller.User.FindFirstValue(ClaimTypes.NameIdentifier), out Guid loggedInUserId))
{
// Get number of unread messages for the logged in user:
int unread = db.MessageRecipients.Where(m =>
m.RecipientUserId == loggedInUserId &&
!m.IsRead).Count();
controller.ViewBag.UnreadMessages = unread;
}
}
}
Registered in Startup.cs like this:
services.AddMvc(options =>
{
options.Filters.Add(typeof(ViewBagFilter));
});
How would I write a PageFilter that could give me access to the same data in my razor pages? I am aware that ViewBag is not available to razor pages. Can I use ViewData? How? If not, what would I use instead?
This is what I have so far:
public class CustomPageFilter : IAsyncPageFilter
{
private readonly ProjectsDbContext db;
public CustomPageFilter(ProjectsDbContext _db)
{
db = _db;
}
public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
await Task.CompletedTask;
}
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
// this has to be changed:
var controller = context.Controller as Controller;
// this has to be changed:
if (Guid.TryParse(controller.User.FindFirstValue(ClaimTypes.NameIdentifier), out Guid loggedInUserId))
{
// Get number of unread messages for the logged in user:
int unread = db.MessageRecipients.Where(m =>
m.RecipientUserId == loggedInUserId &&
!m.IsRead).Count();
// this has to be changed:
controller.ViewBag.UnreadMessages = unread;
}
await next.Invoke();
}
}
And lastly, how do I register the PageFilter in Startup?
I have been reading this guide, and they register their filter like this:
services.RazorPages(options =>
{
options.Filters.Add(new CustomPageFilter(new GeoService()));
});
I am having several problems with that:
IServiceCollection does not contain a definition for RazorPages. If I change it to AddRazorPages, I get another error:
RazorPagesOptions does not contain a definition for Filters.
From what I understand, I have to pass an instance of my db-context to the PageFilter. How do I do that? This does not work: options.Filters.Add(new CustomPageFilter(new ProjectsDbContext()));
Insignificant update
I found out that I can set a ViewData["something"] in the code-behind files for each razor page. But I don't want to add that code to every Identity razor page, so the need for a filter remains.
I found an alternative solution. Please tell me if it is a good one.
I made a new class, ExtendedPageModel, which inherits from PageModel, in which I am setting the ViewData in the method ´SetViewData()´:
public class ExtendedPageModel : PageModel
{
protected ProjectsDbContext db;
private readonly IHttpContextAccessor httpContextAccessor;
public ExtendedPageModel(ProjectsDbContext _db, IHttpContextAccessor _httpContextAccessor)
{
db = _db;
httpContextAccessor = _httpContextAccessor;
}
public void SetViewData()
{
var userId = httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (Guid.TryParse(userId, out Guid loggedInUserId))
{
int unread = db.MessageRecipients.Where(m =>
m.RecipientUserId == loggedInUserId &&
!m.IsRead).Count();
ViewData["UnreadMessages"] = unread;
}
}
}
Then, registered IHttpContextAccessor in Startup:
services.AddHttpContextAccessor();
And lastly, modifying each of the pagemodels. (A bit tedious.) This is /Identity/Pages/Manage/Index.cshtml.cs:
Class header:
public partial class IndexModel : ExtendedPageModel
Constructor (don't mind IWebHostEnvironment - I'm using that for somethig else):
public IndexModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ProjectsDbContext context,
IHttpContextAccessor httpContextAccessor,
IWebHostEnvironment env) : base(context, httpContextAccessor)
{
_userManager = userManager;
_signInManager = signInManager;
db = context;
webHostEnvironment = env;
}
OnGetAsync():
public async Task<IActionResult> OnGetAsync()
{
SetViewData();
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Could not find user with ID '{_userManager.GetUserId(User)}'.");
}
await LoadAsync(user);
return Page();
}
My base Request class looks like this:
public class GetAllProjectsQuery : QueryBase<ProjectsListModel>
{
}
public abstract class QueryBase<T> : UserContext, IRequest<T> // IRequest is MediatR interface
{
}
public abstract class UserContext
{
public string ApplicationUserId { get; set; } // and other properties
}
I want to write a middleware to my .NET Core 3.1 WebApi that will grab JWT from request header amd read ApplicationUserId from it. I started to code something:
public class UserInformation
{
private readonly RequestDelegate next;
public UserInformation(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var jwt = context.Request.Headers["Authorization"];
// read jwt here
var userContext = (UserContext)context.Request.Body; // i know it wont work
userContext.ApplicationUserId = //whats next? Any ideas?
await this.next(context);
}
}
But to be honest i have no idea how to start so here are my questions:
As you can see, every request will be packed with my UserContext class and so on. How to cast HttpContext.Request.Body to my request object and attach ApplicationUserId to it? Is it possible? I want to acces to user credentials from my JWT from headers and i want to have that information in every request in my API (pass it to controller, then to command etc).
If getting this information from middleware is not the best practice, what is?
EDIT: Mcontroller that using MediatR:
// base controller:
[ApiController]
[Route("[controller]")]
public abstract class BaseController : ControllerBase
{
private IMediator mediator;
protected IMediator Mediator => this.mediator ?? (this.mediator = HttpContext.RequestServices.GetService<IMediator>());
}
// action in ProjectControlle
[HttpGet]
[Authorize]
public async Task<ActionResult<ProjectsListModel>> GetAllProjects()
{
return Ok(await base.Mediator.Send(new GetAllProjectsQuery()));
}
// query:
public class GetAllProjectsQuery : QueryBase<ProjectsListModel>
{
}
// handler:
public class GetAllProjectsQueryHandler : IRequestHandler<GetAllProjectsQuery, ProjectsListModel>
{
private readonly IProjectRepository projectRepository;
public GetAllProjectsQueryHandler(IProjectRepository projectRepository)
{
this.projectRepository = projectRepository;
}
public async Task<ProjectsListModel> Handle(GetAllProjectsQuery request, CancellationToken cancellationToken)
{
var projects = await this.projectRepository.GetAllProjectsWithTasksAsync();
return new ProjectsListModel
{
List = projects
};
}
}
You might not need a middleware, but need a model binder:
See: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1
Also see: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-3.1
public class UserContextModelBinder : IModelBinder
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IModelBinder _defaultModelBinder;
public UserContextModelBinder(
IHttpContextAccessor httpContextAccessor,
IOptions<MvcOptions> mvcOptions,
IHttpRequestStreamReaderFactory streamReaderFactory)
{
_httpContextAccessor = httpContextAccessor;
_defaultModelBinder = new BodyModelBinder(mvcOptions.Value.InputFormatters, streamReaderFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(UserContext).IsAssignableFrom(bindingContext.ModelType))
{
return;
}
await _defaultModelBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet && bindingContext.Result.Model is UserContext)
{
var model = (UserContext)bindingContext.Result.Model;
var httpContext = _httpContextAccessor.HttpContext;
// Read JWT
var jwt = httpContext.Request.Headers["Authorization"];
model.ApplicationUserId = jwt;
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
Then add model binder to UserContext class:
[ModelBinder(typeof(UserContextModelBinder))]
public abstract class UserContext
{
public string ApplicationUserId { get; set; }
}
Also add IHttpContextAccessor to services in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpContextAccessor();
}
I'm trying implement a simple dependency (in ASP.NET Core) as this:
public partial class BaseController : Controller
{
public new ITempDataDictionary TempData { get; private set; }
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
//preparação da tempdata
this.TempData = new TempDataDictionary(HttpContext); //todo: DI?
this.TempData.Load();
}
}
}
The problem is the fact TempDataDictionary depends of HttpContext present in this controller.
How to implement that scenario in DI, since the ServiceLocator has no knowledge of HttpContext at Startup?
As this?
services.AddScoped(); //??????
But where i fill the constructor parameter HttpContext if this present just in controller?
You should create a service to handle your state data and add it as scoped.
public class AppStateService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITempDataProvider _tempDataProvider;
private IDictionary<string, object> _data;
public AppStateService(IHttpContextAccessor httpContextAccessor, ITempDataProvider tempDataProvider, UserManager<EntsogUser> userManager, CompanyRepository companyRepository)
{
_httpContextAccessor = httpContextAccessor;
_tempDataProvider = tempDataProvider;
_data = _tempDataProvider.LoadTempData(_httpContextAccessor.HttpContext);
}
private void SetValue(string name, object value)
{
_data[name] = value;
_tempDataProvider.SaveTempData(_httpContextAccessor.HttpContext,_data);
}
private object GetValue(string name)
{
if (!_data.ContainsKey(name))
return null;
return _data[name];
}
}
In Startup.cs (ConfigureServices)
services.AddScoped<AppStateService>();
In your controller
public class TestController : Controller
{
protected readonly CompanyRepository _companyRepository;
public TariffsController(AppStateService appStateService)
{
_appStateService = appStateService;
}
}
you can take a dependency on IHttpContextAccessor and register it with DI
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
then use it to get the HttpContext
However in a controller you have direct access to HttpContext so it isn't clear to me why you would want to inject it there