How to use fallback routing in asp.net core? - 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).

Related

How to disable direct access to a view from url?

I have two views in my asp net core application. The first view is called customer and the second view is called payment. I want to disable that users can get direct acces by typing the url "https://mywebsite/Payment" in the browser.
I want the users to be redirected to view which is called customer If users are trying to get direct access to view called payment.
How can I do that. I don't have any idea.
You could create a filter as below :
public class NoDirectAccessAttribute:ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var canAccess = false;
//check the refer
var referer = context.HttpContext.Request.Headers["Referer"].ToString();
if(!string.IsNullOrEmpty(referer))
{
var request = context.HttpContext.Request;
var rUri = new System.UriBuilder(referer).Uri;
if(request.Host.Host==rUri.Host && request.Host.Port==rUri.Port && request.Scheme==rUri.Scheme)
{
canAccess = true;
}
}
// ... check other requirements
if (!canAccess)
{
context.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "Index", area = "" }));
}
}
}
Then you can apply NoDirectAccess Attribute to specific Action
[NoDirectAccess]
public IActionResult Privacy()
{
return View();
}

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
}

Localized page names with ASP.NET Core 2.1

When create a Razor page, e.g. "Events.cshtml", one get its model name set to
#page
#model EventsModel
where the page's name in this case is "Events", and the URL would look like
http://example.com/Events
To be able to use page name's in Norwegian I added the following to the "Startup.cs"
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options => {
options.Conventions.AddPageRoute("/Events", "/hvaskjer");
options.Conventions.AddPageRoute("/Companies", "/bedrifter");
options.Conventions.AddPageRoute("/Contact", "/kontakt");
});
With this I can also use an URL like this and still serve the "Events" page
http://example.com/hvaskjer
I'm planning to support many more languages and wonder, is this the recommended way to setup localized page name's/route's?, or is there a more proper, correct way to accomplish the same.
I mean, with the above sample, and having 15 pages in 10 languages it gets/feels messy using options.Conventions.AddPageRoute("/Page", "/side"); 150 times.
You can do this with the IPageRouteModelConvention interface. It provides access to the PageRouteModel where you can effectively add more templates for routes to match against for a particular page.
Here's a very simple proof of concept based on the following service and model:
public interface ILocalizationService
{
List<LocalRoute> LocalRoutes();
}
public class LocalizationService : ILocalizationService
{
public List<LocalRoute> LocalRoutes()
{
var routes = new List<LocalRoute>
{
new LocalRoute{Page = "/Pages/Contact.cshtml", Versions = new List<string>{"kontakt", "contacto", "contatto" } }
};
return routes;
}
}
public class LocalRoute
{
public string Page { get; set; }
public List<string> Versions { get; set; }
}
All it does is provide the list of options for a particular page. The IPageRouteModelConvention implementation looks like this:
public class LocalizedPageRouteModelConvention : IPageRouteModelConvention
{
private ILocalizationService _localizationService;
public LocalizedPageRouteModelConvention(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void Apply(PageRouteModel model)
{
var route = _localizationService.LocalRoutes().FirstOrDefault(p => p.Page == model.RelativePath);
if (route != null)
{
foreach (var option in route.Versions)
{
model.Selectors.Add(new SelectorModel()
{
AttributeRouteModel = new AttributeRouteModel
{
Template = option
}
});
}
}
}
}
At Startup, Razor Pages build the routes for the application. The Apply method is executed for every navigable page that the framework finds. If the relative path of the current page matches one in your data, an additional template is added for each option.
You register the new convention in ConfigureServices:
services.AddMvc().AddRazorPagesOptions(options =>
{
options.Conventions.Add(new LocalizedPageRouteModelConvention(new LocalizationService()));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

Multiple Access Denied Pages

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();
}
}

How to force re authentication between ASP Net Core 2.0 MVC web app and Azure AD

I have an ASP.Net Core MVC web application which uses Azure AD for authentication. I have just received a new requirement to force user to reauthenticate before entering some sensitive information (the button to enter this new information calls a controller action that initialises a new view model and returns a partial view into a bootstrap modal).
I have followed this article which provides a great guide for achieving this very requirement. I had to make some tweaks to get it to work with ASP.Net Core 2.0 which I think is right however my problems are as follows...
Adding the resource filter decoration "[RequireReauthentication(0)]" to my controller action works however passing the value 0 means the code never reaches the await.next() command inside the filter. If i change the parameter value to say 30 it works but seems very arbitrary. What should this value be?
The reauthentication works when calling a controller action that returns a full view. However when I call the action from an ajax request which returns a partial into a bootstrap modal it fails before loading the modal with
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'https://localhost:44308' is therefore not allowed
access
This looks like a CORS issue but I don't know why it would work when going through the standard mvc process and not when being called from jquery. Adding
services.AddCors();
app.UseCors(builder =>
builder.WithOrigins("https://login.microsoftonline.com"));
to my startup file doesn't make any difference. What could be the issue here?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Ommitted for clarity...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.AddCors();
// Ommitted for clarity...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Ommitted for clarity...
app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
AzureAdAuthenticationBuilderExtensions.cs
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("auth_time");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
});
return builder;
}
private static Task RedirectToIdentityProvider(RedirectContext context)
{
// Force reauthentication for sensitive data if required
if (context.ShouldReauthenticate())
{
context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
}
else
{
context.Properties.RedirectUri = new PathString("/Account/SignedIn");
}
return Task.FromResult(0);
}
internal static bool ShouldReauthenticate(this RedirectContext context)
{
context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);
bool shouldReauthenticate = false;
if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
{
throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
}
return shouldReauthenticate;
}
// Ommitted for clarity...
}
RequireReauthenticationAttribute.cs
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst("auth_time")?.Value, out int authTime);
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (foundAuthTime && ts - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));
}
}
}
CreateNote.cs
[HttpGet]
[RequireReauthentication(0)]
public IActionResult CreateNote(int id)
{
TempData["IsCreate"] = true;
ViewData["PostAction"] = "CreateNote";
ViewData["PostRouteId"] = id;
var model = new NoteViewModel
{
ClientId = id
};
return PartialView("_Note", model);
}
Razor View (snippet)
<a asp-controller="Client" asp-action="CreateNote" asp-route-id="#ViewData["ClientId"]" id="client-note-get" data-ajax="true" data-ajax-method="get" data-ajax-update="#client-note-modal-content" data-ajax-mode="replace" data-ajax-success="ShowModal('#client-note-modal', null, null);" data-ajax-failure="AjaxFailure(xhr, status, error, false);"></a>
All help appreciated. Thanks
The CORS problem is not in your app.
Your AJAX call is trying to follow the authentication redirect to Azure AD,
which will not work.
What you can do instead is in your RedirectToIdentityProvider function, check if the request is an AJAX request.
If it is, make it return a 401 status code, no redirect.
Then your client-side JS needs to detect the status code, and issue a redirect that triggers the authentication.