Multiple Access Denied Pages - asp.net-core

I'm creating an application that has two different authorization scenarios: admin and site.
If you try to access a /admin route without the policy succeeding the user should be redirected to an access denied page. At this point there's no action the user can take. They can't access the resource and there's nothing for them to do.
If you try to access a /site/joes-super-awesome-site route without the policy suceeding the user should be redirected to a different access denied. At this point the user should be able to request access. There is an action they can take.
What's the best way to achieve this? I know I can override the default OnRedirectToAccessDenied action but that will require some ugly string parsing (untested example below).
.AddCookie(options => {
options.Events.OnRedirectToAccessDenied = context => {
// parsing this kinda sucks.
var pathParts = context.Request.Path.Value.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathParts?[0] == "site") {
context.Response.Redirect($"/{pathParts[0]}/request-access");
} else {
context.Response.Redirect("/account/access-denied");
}
return Task.CompletedTask;
};
})

Doing some searching, I found the following information:
Someone with the same question on this GitHub issue
Tracking of authorization-related improvements in this GitHub issue
Unfortunately these improvements didn't make it to ASP.NET Core 2.1.
It seems that at this point, another option (apart from your suggestion of parsing the request URL) is to imperatively invoke the authorization service in your MVC actions.
It could go from:
// Highly imaginary current implementation
public class ImaginaryController : Controller
{
[HttpGet("site/{siteName}")]
[Authorize("SitePolicy")]
public IActionResult Site(string siteName)
{
return View();
}
[HttpGet("admin")]
[Authorize("AdminPolicy")]
public IActionResult Admin()
{
return View();
}
}
to:
public class ImaginaryController : Controller
{
private readonly IAuthorizationService _authorization;
public ImaginaryController(IAuthorizationService authorization)
{
_authorization = authorization;
}
[HttpGet("site/{siteName}")]
public Task<IActionResult> Site(string siteName)
{
var sitePolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "SitePolicy");
if (!sitePolicyAuthorizationResult.Success)
{
return Redirect($"/site/{siteName}/request-access");
}
return View();
}
[HttpGet("admin")]
public Task<IActionResult> Admin()
{
var adminPolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "AdminPolicy");
if (!adminPolicyAuthorizationResult.Success)
{
return Redirect("/account/access-denied");
}
return View();
}
}

Related

How to use fallback routing in asp.net core?

I am using asp.net web-api with controllers.
I want to do a user section where one can request the site's address with the username after it like example.com/username. The other, registered routes like about, support, etc. should have a higher priority, so if you enter example.com/about, the about page should go first and if no such about page exists, it checks whether a user with such name exists. I only have found a way for SPA fallback routing, however I do not use a SPA. Got it working manually in a middleware, however it is very complicated to change it.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string[] internalRoutes = new string[] { "", "about", "support", "support/new-request", "login", "register" };
string[] userNames = new string[] { "username1", "username2", "username3" };
app.Use(async (context, next) =>
{
string path = context.Request.Path.ToString();
path = path.Remove(0, 1);
path = path.EndsWith("/") ? path[0..^1] : path;
foreach (string route in internalRoutes)
{
if (route == path)
{
await context.Response.WriteAsync($"Requested internal page '{path}'.");
return;
}
}
foreach (string userName in userNames)
{
if (userName == path)
{
await context.Response.WriteAsync($"Requested user profile '{path}'.");
return;
}
}
await context.Response.WriteAsync($"Requested unknown page '{path}'.");
return;
await next(context);
});
app.Run();
It's really straightforward with controllers and attribute routing.
First, add controller support with app.MapControllers(); (before app.Run()).
Then, declare your controller(s) with the appropriate routing. For simplicity, I added a single one that just returns simple strings.
public class MyController : ControllerBase
{
[HttpGet("/about")]
public IActionResult About()
{
return Ok("About");
}
[HttpGet("/support")]
public IActionResult Support()
{
return Ok("Support");
}
[HttpGet("/support/new-request")]
public IActionResult SupportNewRequest()
{
return Ok("New request support");
}
[HttpGet("/{username}")]
public IActionResult About([FromRoute] string username)
{
return Ok($"Hello, {username}");
}
}
The routing table will first check if there's an exact match (e.g. for /about or /support), and if not, if will try to find a route with matching parameters (e.g. /Métoule will match the /{username} route).

User re verification page .net core

A page that asks the already signed in user to confirm their password one more time for security purposes on certain actions. Once confirmed it will go back to whatever request(action)they made in the first place. Should I use an user API for this? How can I achieve something like this?
Public IActionResult IndexMethod()
{
//process request only if user was verified using that verification page.
//It can take in parameters such as tokens if needed
}
In my opinion, if you want to confirm their password one more time for security purposes on certain actions. I suggest you could try to use action filter instead of directly going to the action and you could store the previous url into session.
More details, you could refer to below test demo:
1.Enable session:
Add below codes into Startup.cs's ConfigureServices method:
services.AddSession();
Add below codes into Configure method:
app.UseSession();
2.Create a filter:
public class ConfirmActionFilter : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
//We will store the user is comfirmed into session and check it at the filter
if (String.IsNullOrEmpty(context.HttpContext.Session.GetString("checked")))
{
//store the path into session route .
context.HttpContext.Session.SetString("route", context.HttpContext.Request.Path);
//redirect to the confrim controller action
context.Result = new RedirectToActionResult("Index", "Confirm", context.HttpContext.Request.RouteValues);
}
}
}
3.Add confirm controller:
public class ConfirmController : Controller
{
public IActionResult Index()
{
//You could get the path
HttpContext.Session.SetString("checked","true");
return View();
}
public IActionResult Checked() {
// redirect to the path user has accessed.
var re = HttpContext.Session.GetString("route");
return new RedirectResult(re);
}
}
filter usage:
[ConfirmActionFilter]
public class HomeController : Controller
Result:
If the user access firstly, you will find it will go to the confirm method.

Policy-Based Authorization to enable only Users who are owners to a resource be able to view/edit

I have a simple requirement of checking if a signed-in user is the owner of a resource before allowing the viewing/editing of the resource.
I have been able to accomplish this with checking in each ActionResult but would love to use the policy-based authorization to achieve this, documentation is silent on this particular requirement.
public IActionResult EditPage(int id)
{
//Check User is the Owner of the resource
var signedInUserResouceID = ((ClaimsIdentity)User.Identity).FindFirst("ResourceID");
if(id != signedInUserResourceID)
{
//User cannot Edit this resouce
return RedirectToAction("Index","Home");
}
else
{
//Rightful owner, allow editing
}
}
How do I use Policy-based authorization in achieving this
Add the following code to your Startup.cs:
services.AddHttpContextAccessor();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy =>
{
policy.RequireAssertion(context =>
{
//Here you can get many resouces from context, i get a claim here for example
var yourvalue = context.User.FindFirst("ResourceID").Value;
//here you can get the query string value
var id = new HttpContextAccessor().HttpContext.Request.Query["id"];
//return a boolen to end validation.
return id==yourvalue;
});
});
});
Controller:
[Authorize(Policy = "EmployeeOnly")]
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
public IActionResult EditPage(int id)
{
//...
}
}
Putting 'ResourceID' into user claims is not the way to achieve that.
Take a look at this article, it explains in details to to implement resource based authorization:
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1

Generic string router with DB in Asp.net Core

I am creating an internet store. And I want to add short URLs for products, categories and so on.
For example:
store.com/iphone-7-plus
This link should open the page with iPhone 7 plus product.
The logic is:
The server receives an URL
The server try it against existent routes
If there is no any route for this path - the server looks at a DB and try to find a product or category with such title.
Obvious solutions and why are they not applicable:
The first solution is a new route like that:
public class StringRouter : IRouter
{
private readonly IRouter _defaultRouter;
public StringRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public async Task RouteAsync(RouteContext context)
{
// special loggic
await _defaultRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
}
The problem is I can't provide any access to my DB from StringRouter.
The second solution is:
public class MasterController : Controller
{
[Route("{path}")]
public IActionResult Map(string path)
{
// some logic
}
}
The problem is the server receive literally all callings like store.com/robots.txt
So the question is still open - could you please advise me some applicable solution?
For accessing DbContext, you could try :
using Microsoft.Extensions.DependencyInjection;
public async Task RouteAsync(RouteContext context)
{
var dbContext = context.HttpContext.RequestServices.GetRequiredService<RouterProContext>();
var products = dbContext.Product.ToList();
await _defaultRouter.RouteAsync(context);
}
You also could try Middleware to check whether the reuqest is not exist, and then return the expected response.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
app.Use(async (context,next) => {
await next.Invoke();
// add your own business logic to check this if statement
if (context.Response.StatusCode == 404)
{
var db = context.RequestServices.GetRequiredService<RouterProContext>();
var users = db.Users.ToList();
await context.Response.WriteAsync("Request From Middleware");
}
});
//your rest code
}

MVC5 EF6 How to add confirmation screen with additional authentication before submitting data

Developing a new MVC5 project. I have my scaffolding in place for CRUD functionality but there is a requirement that when data is inserted or updated, an e-signature is required. Before data can be submitted to the database the user must be presented with a page asking them to enter their username and password again to confirm the data. If the username and password entered is valid and the username matches the currently signed in user, then the original data entered can be saved to its table (for example Member) and the e-signature information is saved to a separate table (ESignature). I'd appreciate any help on the best way to go about this - a view model combining Member and ESignature, or a reuse of the LoginViewModel from the Account controller to check the authentication, or an alternative approach? I need something that I can use across half a dozen controllers where e-signatures are required.
Alright maybe my approach is not the best but I will attempt.
My solution would be to create a CustomAttribute: AuthorizeAttribute and decorate all the actions which require Esignature. In your CustomAttribute implementation you will redirect to a controller action exactly similar to Login but with slight modification.
public class CustomAuthorize : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
var url = filterContext.HttpContext.Request.Url;
var query = url.Query;
if (query.Contains("g="))
{
var code = query.Split(new String[] { "g=" }, StringSplitOptions.None);
//You can create time sensistive token and validate it.
}
else
{
//Redirect User to a particular page
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "controller", "Account" },
{ "action", "elogin" },
{ "redirectUrl", url.AbsolutePath}
}
);
}
}
}
Then decorate for example Index() method with it.
[CustomAuthorize]
public ActionResult Index()
{
return View();
}
At first when you hit the Index() method then inside OnAuthorization method of CustomAuthorizeAttribute the else loop gets executed and re-directs you to a elogin method inside AccountController. This method is similar to the Login HttpGet method. While specifying the RedirectToResult I am specifying the redirectUrl path of the current page so when you successfully validate a user inside the elogin method then with the help of redirectUrl we can come back.
[AllowAnonymous]
public ActionResult ELogin(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View("Login");
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ELogin(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
await SignInAsync(user, model.RememberMe);
var url = String.Format("{0}/?g={1}", returnUrl, "HashCode");
return RedirectToLocal(url);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
The only difference in the HttpPost ELogin method is that before doing RedirectToLocal I append /g=HasCode. Note: Here you can append your own logic to create a time sensitive hash. When we get redirected to our home page then we can inspect inside our OnAuthorization Method if the url contains g=HashCode then don't redirect to Login Page.
This would be very basic idea on how you can approach to force users to re-sign in whenever they hit specific controllers. You will have to do additional security checks and be careful in what you are exposing via url.