Validate route data on every request in ASP.Net Core Net 6 - asp.net

I'm currently migrating an ASP.Net MVC 6 app to Core Net6. In addition to the default Controller/Action/Id route, I'm creating an additional route that contains a customerkey in the URL:
app.MapControllerRoute(
name: "ManagingCustomer",
pattern: "{customerkey:int}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "Default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Within the app, a user can manage their own account or other accounts to which they have access. When they are managing an account other than their own, I would navigate with the prepended customerkey route data.
If a URL contains the customerkey route data item, where in the pipeline can I inject code to validate that the currently authenticated user actually has access to the requested account? I would like this run on every request and don't necessarily want to add code to every controller and action in order to support it. In the old MVC6 app, I would have done this in global.asax.cs. I just don't know where this fits in the core world. I'm guessing it falls into middleware? Any help would be appreciated. Thanks.

If you already registed authoriztion policies ,i think you could try as below:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "ManageCustomer",
pattern: "somesection/{controller=Home}/{action=Index}/{id?}").RequireAuthorization("Policyname");
.......
});
And maybe you could try with Fallbackpolicy as this document:
If you just want to get the customerkey and validate it in your middleware,you could try as below:
app.Use(
async (context, next) =>
{
var routevalue = context.Request.RouteValues;
var customerkey = routevalue.ContainsKey("customerkey") ? routevalue["customerkey"].ToString() : "";
//add your logical here
if (customerkey != "")
{
await next.Invoke();
}
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
await context.Response.WriteAsync("ErrorInfo");
});

Related

ASP.NET - Problem with authentication: Manually added claim is missing in the next request

I am currently experimenting with external login providers like Google Authentication in my ASP.NET application.
As you can see on my Program.cs i'am running .NET6.
After the Google-Login was successfull, the ClaimsPrincipal has exactly one ClaimsIdentity (IsAuthenticated == true). Now I want to add my own 'Custom-Claim' to this identity. The addition works without any problems, but the next request is missing the custom claim (all other claims by Google are there).
Here is the part of my Program.cs where I add the authentication:
builder.Services.AddAuthentication(x =>
{
x.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(GoogleDefaults.AuthenticationScheme, o =>
{
o.ClientId = builder.Configuration["Authentication:Google:ClientId"];
o.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
o.ClaimActions.MapJsonKey("urn:google:picture","picture","url");
});
Here is the configuration of the middleware pipeline in Program.cs:
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.Use((httpContext, func) =>
{
if (httpContext.Request.Path.StartsWithSegments("/api"))
httpContext.Request.Headers[HeaderNames.XRequestedWith] = "XMLHttpRequest";
return func();
});
app.UseRouting();
app.MapRazorPages();
app.MapControllers();
Endpoint for Google Login:
[HttpGet]
[Route("GoogleSignIn")]
public async Task GoogleSignIn()
{
await HttpContext.ChallengeAsync(GoogleDefaults.AuthenticationScheme,
new AuthenticationProperties {RedirectUri =Url.Action("GoogleResponse")});
}
In the "GoogleResponse" method i add the mentioned custom claim:
//this method gets called after the google login has finished
public async Task<IActionResult> GoogleResponse()
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var identity = result.Principal.Identities.FirstOrDefault();
var currentSidClaim = identity.FindFirst(x => x.Type == ClaimTypes.Sid);
if (currentSidClaim != null)
identity.RemoveClaim(currentSidClaim);
identity.AddClaim(new Claim(ClaimTypes.Sid, "Hi i am some claim, but i'll miss on the next request :("));
return Redirect("/");
}
ClaimsIdentity after adding the claim:
[https://i.stack.imgur.com/INdGy.png]
ClaimsIdentity on the next request (in the same controller btw):
[https://i.stack.imgur.com/oft3e.png]
Whether I access the identity via "User.Identity" or "HttpContext.User..." makes no difference.
Really hope somebody can clear things up for me.
I hope I don't have to implement a large / elaborate ASP.NET Identity solution to solve the problem. I would be happy with a lightweight authentication.
UPDATE:
After some further tests it looks like it is not necessarily related to the specific Google login. Even if I create a second identity at login, it is gone at the next request.
I guess the authentication cookie is not updated.
Thanks in advance!
Try to add claims after callback from the external website. As described here

How to catch all routes but ignore some URLs?

I'm starting to upgrade my old WebForms CMS which uses a catch all handler.
In .NET Core 2 I have created a route like:
routes => {
routes.MapRoute(
name: "NodeHandler",
template: "{*url}",
defaults: new { controller = "Node", action = "Index" );
}
Which by default catch all. How do I get the route to ignore certain directories?
Attribute-based routing. In your controllers use the [NonAction] attribute so that no route will be created for an action.
The default route (template) is {controller}/{action}/{id} but you can have non-action methods using this technique.
Controllers and Actions

How do you setup special characters for Identity Core LoginPath?

I'm using ASP.NET Identity Core for authentication. I have an AngularJS SPA with a login route (UI-Router). My user's are getting routed to an escaped URL instead of the real URL.
My login path is set to "/#!/login", but my users are being routed to: "/%23!/login" which causes a 401 in the browser.
I've tried using System.Uri.EscapeDataString, System.Uri.EscapeUriString and without escaping at all with no luck.
Startup.cs
services.AddAuthentication()
.AddCookie(cookie =>
{
cookie.LoginPath = System.Uri.EscapeDataString("/#!/login");
})
AngularJS Route
.state('login', {
url: 'login',
views: {
'': { templateUrl: './Home/login.html', controller: "loginController" }
}
})
I've confirmed the server is generating a 302 response with the following location: "http://localhost:63939/%23!/login?ReturnUrl=%2Fadministrator". So the server is definitely escaping the "#!" and it is not something the browser is doing.
The only thing I've been able to figure out is to unescape the RedirectUri in the OnRedirectToLogin cookie authentication event. There has got to be a better way.
cookie.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = context =>
{
context.Response.Redirect(System.Uri.UnescapeDataString(context.RedirectUri));
return Task.FromResult(0);
}
};

ASP.NET Core seperate middleware for area controllers

I am building a prototype whereby I host my ASP.NET Core website (standard controllers/views etc) and the API within the same project.
I wish to use the following route scheme:
blah.com/xxxx - website controllers and actions.
blah.com/api/xxxx - api controllers and actions.
My approach thus far is to look at areas and the route config below works perfectly:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaDefault",
template: "{area:exists}/{controller=Values}/{action=Index}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Now I want to expand this to use one set of middleware when accessing the API and another set when accessing the website. In reality this is to use different authentication setups between the areas. Google-fu lead me to the IApplicationBuilder.Map method and this is where my problem lies.
The config below works for the default route, Home/Index is executed but anything after /api returns 404.
// TODO: Add common middleware.
app.Map("/api", builder =>
{
// TODO: Add api specific middleware.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "apiDefault",
template: "api/{controller=Values}/{action=Index}");
});
});
// TODO: Add website specific middleware.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Routes i have tried without success are:
foo.com/api
foo.com/api/values
foo.com/api/values/index
foo.com/api/api
foo.com/api/api/values
foo.com/api/api/values/index
The ValuesController is under the folder Areas/Api/Controllers and is defined as:
[Area("api")]
public class ValuesController : Controller
{
public IActionResult Index()
{
return Json(new { test = 1 });
}
}
The full source to reproduce this is available here: https://github.com/AntSwift/ApiRouteTest
Am I heading down the right path with this, is there something obvious I'm missing or is what I am attempting simply not possible.
I suggest to do it differently and don't mix them together. Just create your api area or use api routing the way you want. Then create a middleware and name it "ApiAuthenticationMiddleware". Put it somewhere at the top of the configure pipeline. In that middleware look at the different properties in httpcontext . There are a lot of useful values in httpcontext. In your case you could use "path" property to find out where the request are going.
If the path has "/api/" in it. It means its going to your api's. Otherwise its going to your normal controllers. In that case you should short-circuit to next step in the pipeline.
Again in your case you could use below snippet in your middleware as an example. You should change it the way you want:
public async Task Invoke(HttpContext httpContext, YourDbContext dbContext)
{
string requestPath = httpContext.Request.Path.Value;
if(!requestPath.Contains("/api/"))
await _next.Invoke(httpContext);
string authHeader = httpContext.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic"))
{
//Extract credentials
// other stuff
// example when no authorization header exists and want to reject.
httpContext.Response.StatusCode = 401; //Unauthorized
return;
// Call the next delegate/middleware in the pipeline
await _next.Invoke(httpContext);
}
}
Check this, in the map section replace app.UseMvc with builder.UseMvc
app.Map("/api", builder =>
{
// TODO: Add api specific middleware.
app.UseMvc(routes => // should be builder.UseMvc(routes
{
routes.MapRoute(
name: "apiDefault",
template: "api/{controller=Values}/{action=Index}");
});
});

ASP.NET 5 + Angular 2 routing (template page not REloading)

Angular 2 beta uses html5 routing by default.
However, when you go to a component and the route changes (eg http://localhost:5000/aboutus) and you reload/refresh the page, nothing is loaded.
The issue has been raised in this post also.
Most of the answers say that if we are going to pursue HTML5 routing in angular 2, then this issue of routing should be taken care of in server-side. More discussion here.
I am not sure how to handle this issue using the asp.net server environment.
Any angular 2 devs out there who also uses asp.net and encounters this issue?
PS. I'm using ASP.NET 5. My Angular 2 routes are using MVC routes.
The problem you're seeing has to do with the difference between Angular routing on the client and MVC server-side routing. You are actually getting a 404 Page Not Found error because the server does not have a Controller and Action for that route. I suspect you are not handling errors which is why it appears as if nothing happens.
When you reload http://localhost:5000/aboutus or if you were to try to link to that URL directly from a shortcut or by typing it into the address bar (deep linking), it sends a request to the server. ASP.NET MVC will try to resolve that route and in your case it will try to load the aboutusController and run the Index action. Of course, that's not what you want, because your aboutus route is an Angular component.
What you should do is create a way for the ASP.NET MVC router to pass URLs that should be resolved by Angular back to the client.
In your Startup.cs file, in the Configure() method, add an "spa-fallback" route to the existing routes:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// when the user types in a link handled by client side routing to the address bar
// or refreshes the page, that triggers the server routing. The server should pass
// that onto the client, so Angular can handle the route
routes.MapRoute(
name: "spa-fallback",
template: "{*url}",
defaults: new { controller = "Home", action = "Index" }
);
});
By creating a catch-all route that points to the Controller and View that ultimately loads your Angular app, this will allow URLs that the server does not handle to be passed onto the client for proper routing.
In your Startup.cs add this to the Configure method. This must be before other app statements.
app.Use(async (context, next) => {
await next();
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) {
context.Request.Path = "/index.html"; // Put your Angular root page here
await next();
}
});
My favorite solution is to add the following code to Global.asax.cs which very smoothly and reliably takes care of the issue:
private const string RootUrl = "~/Home/Index";
// You can replace "~Home/Index" with whatever holds your app selector (<my-app></my-app>)
// such as RootUrl="index.html" or any controller action or browsable route
protected void Application_BeginRequest(Object sender, EventArgs e)
{
// Gets incoming request path
var path = Request.Url.AbsolutePath;
// To allow access to api via url during testing (if you're using api controllers) - you may want to remove this in production unless you wish to grant direct access to api calls from client...
var isApi = path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase);
// To allow access to my .net MVCController for login
var isAccount = path.StartsWith("/account", StringComparison.InvariantCultureIgnoreCase);
if (isApi || isAccount)
{
return;
}
// Redirects to the RootUrl you specified above if the server can't find anything else
if (!System.IO.File.Exists(Context.Server.MapPath(path)))
Context.RewritePath(RootUrl);
}
You need use this routing in ASP.NET MVC
app.UseMvc(routes =>
{
routes.MapRoute("Default", "{*url}", new { #controller = "App", #action = "Index" });
});
Then you need set up SystemJS with basePath options
The feature you're looking for is URL rewrite. There are two possible ways to handle it. The classic way is to let IIS do the work, as described here:
https://stackoverflow.com/a/25955654/3207433
If you don't want to depend on IIS, you can instead handle this in the ASP.NET 5 middleware, as shown in my answer here:
https://stackoverflow.com/a/34882405/3207433
I'm not having any luck getting
routes.MapRoute("Default", "{*url}",
new { #controller = "App", #action = "RedirectIndex" });
to work. I still get a 404 with any client side route.
Update:
Figured out why the catch-all route wasn't working: I had an attribute route defined ([Route("api/RedirectIndex")]) and while the plain route can be directly accessed with the fallback route it didn't fire. Removing the attribute route made it work.
Another solution that seems to work just as easy as the catch-all route handler is to just create a custom handler that fires at the end of the middleware pipeline in Configure():
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//handle client side routes
app.Run( async (context) =>
{
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync(Path.Combine(env.WebRootPath,"index.html"));
});
This basically ends up being the catch-all route that simply sends index.html out over the existing URL request if there was no other handler that picked up the request.
This works nicely even in combination with IIS Rewrite rules (in which case the above just won't ever get fired.
Wrote up a blog post on this topic:
Handling HTML5 Client Route Fallbacks in ASP.NET Core
Here are two more options for solving this problem. You can either add the hash location strategy to your app module.
import { LocationStrategy, HashLocationStrategy } from '#angular/common';
#NgModule({
imports: [.... ],
declarations: [...],
bootstrap: [AppComponent],
providers: [
{
provide: LocationStrategy,
useClass: HashLocationStrategy
}
]
})
export class AppModule { }
This option will only work for the parts of your Angular2 app that live on the Home ASP Controller
Your second option is to add routes to your ASP Controller that match your Angular 2 app routes and return the "Index" View
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[ActionName("Angular-Route1")]
public IActionResult AngularRoute1()
{
return View("Index");
}
public IActionResult Route2()
{
return View("Index");
}
}
Did you use:
directives: [RouterOutlet, RouterLink] in the component.
apply the #ZOXEXIVO's solution then, in your _Layout.cshtml add this:
<head>
<base href="/"/>
.....
</had>
You can use both the routing
when you call Home/Index from angular routing.
write
Home/Index.cshtml
<my-app></my-app>
app.routing.ts
const routes: Routes = [
{ path: '', redirectTo: '/Home/Index', pathMatch: 'full' },
{ path: 'Home/Index', component: DashboardComponent }
]
So When URL will be Home/Index
will load the component of active url so it will load dashboard component.
The above selected solution did not work for me I also got 404 after following all the comments to the T. I am using an angular5 app in an MVC5 app. I use the default index landing page as the start for the angular5. My angular app is in a folder named mvcroot/ClientApp/ but on ng build it puts the distributed files in mvcroot/Dist/ by altering one setting in the .angular-cli.json file with "outDir": "../Dist"
This solution did work though.
This way only routes in the Dist directory get the fall over. Now you can hit refresh every time and exact route for the angular5 app reloads while staying on the correct component. Be sure to put the catch all first. On a side note, if using a token auth in your angular5, save the token to window.localStorage (or some other mechanism outside your angular5 app) as hitting refresh will wipe out all memory where you you maybe storing your token in a global variable. This keeps the user from having to login again if they refresh.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Catch All",
"dist/{*url}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Resources