Static files can require the user to be authenticated as per documentation
I have not been able to find any info on restricting authorized access to static files, according to specific claims.
E.g. users with claims "A" and "B" have access to folder A and B, where as users with only claim "B" only have access to folder B
How would I accomplish this "as easy as possible" with .NET 6.0 / webAPI / static files?
Currently there is no built-in way to secure wwwroot directories, I think you can expose an endpoint, and then make judgments in the endpoint, This is a very simple method as you expected, in your question, you want to access static file A only user with claims A,I write a similar demo here, hope it can help you to solve your problem.
First I have a static file named "AAA" in wwwroot.
I use Asp.Net Core Identity here, Now I am logged in as a user, Then I add claim to this user.
//the claim's type and value is the same with static file name
Claim claim = new Claim("AAA", "AAA");
await _userManager.AddClaimAsync(user,claim);
Then I expose an endpoint to get the static path then do judgments :
//Add [Authorize] attribute, the controller can only be accessed when the user is logged in
[Authorize]
public class TestController : Controller
{
//Pass in the name of the static file that needs to be accessed, and then use claim to authorize
public IActionResult Find(string path)
{
var value = IHttpContextAccessor.HttpContext.User.Claims.Where(e => e.Type == path ).Select(e => e.Value).FirstOrDefault();
if(value !=null && value == path) {
//authorize success
//read the static file and do what you want
}else{
//authorize fail
}
}
}
View
//use asp-route-path="AAA" to pass the value of path
<a asp-controller="Test" asp-action="Find" asp-route-path="AAA">AAA</a>
<a asp-controller="Test" asp-action="Find" asp-route-path="BBB">BBB</a>
//.......
From the linked example;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
You could build any policy you want, by calling any of the .Require... methods. eg;
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireClaim("name", "value")
.Build();
});
Note that the fallback policy applies to all endpoints that don't have any [Authorize] metadata.
Instead, you will probably need to write some middleware to check your authorization rule for each path. Perhaps based on this sample.
The linked example demonstrates an interesting concept. Authorisation is based on endpoints, but the static file middleware just takes over the response without using endpoint routing. So what if we generated our own endpoint metadata based on the file provider;
.Use((context, next) => { SetFileEndpoint(context, files, null); return next(context); });
That's doable, but what if we just defined a fake endpoint?
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.UseEndpoints(endpoints => {
endpoints.MapGet("static/pathA/**",
async (context) => context.Response.StatusCode = 404)
.RequireAuthorization("PolicyA");
});
Of course you could map that dummy path to a controller.
Related
What I'm trying to do is very simple, at least so it seemed at first.
I need to protect static HTML files (and possibly other static resources) behind simple Cookie Authentication. Any request to these files from user agent without carrying authentication cookie should be rejected with return HTTP 401 status code.
And I need to do this on top of Owin because of project restriction. I'm dealing with legacy project which have not yet migrate to ASP.NET Core.
Here's my Project directory structure.
OwinWebProject
|- wwwroot
| |- index.html
| |- login.html
|- Startup.cs
|- Web.config
Below is my Owin Startup class. What I do here is handling these paths:
/login which simply establish wrap some simple claims in an identity, and then tells the Authentication Manager to set the authentication state if that identity as 'logged in'. No actual identity checks here for simplicity
/logout does the opposite of /login. It clears the authentication state.
/secret to test whether /login and /logout works
/hello just for testing if Owin middlewares are properly configured.
I also use Microsoft.Owin.StaticFiles.FileServerExtensions to serve my static files.
// using statements omitted
[assembly: OwinStartup(typeof(OwinWebProject.Startup))]
namespace OwinWebProject
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
// BEGIN Configure Cookie Authentication
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieName = "MyApp",
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
SlidingExpiration = true,
});
// END Configure Cookie Authentication
// BEGIN Configure File Server to serve static resources
var wwwRootFS = new PhysicalFileSystem(#".\wwwroot");
var fileServerOpts = new FileServerOptions
{
EnableDefaultFiles = true,
FileSystem = wwwRootFS,
};
fileServerOpts.DefaultFilesOptions.DefaultFileNames = new[] { "index.html" };
fileServerOpts.StaticFileOptions.ServeUnknownFileTypes = true;
app.UseFileServer(fileServerOpts);
// END Configure File Server
// BEGIN create route handlers for
// setting the auth state (i.e logging in),
// clearing the auth state (i.e logging out),
// and for testing authentication state
// this route is unguarded
app.Map("/hello", (config) =>
config.Run(context => {
return context.Response.WriteAsync("It works!");
})
);
// this route is for testing authentication state
app.Map("/secret", (config) =>
config.Run(context => {
if (context.Request.User == null || !context.Request.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = 401;
return Task.FromResult(0);
}
return context.Response.WriteAsync("If you can read this, it means you have logged in");
})
);
// this route is for setting the authentication state
app.Map("/login", (config) =>
config.Run(context => {
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, "fakeuser"));
var authManager = context.Authentication;
authManager.SignIn(identity);
context.Response.Redirect("/index.html");
return Task.FromResult(0);
})
);
// this route is for clearing the authentication state
app.Map("/logout", (config) =>
config.Run(context => {
var authManager = context.Authentication;
authManager.SignOut(CookieAuthenticationDefaults.AuthenticationType);
context.Response.Redirect("/");
return Task.FromResult(0);
})
);
// END create route handlers
}
}
}
Here is index.html.
<html>
<body>
If you can see this that mean you are authenticated
<p>Logout</p>
</body>
</html>
Here is login.html
<html>
<body>
<p>Click here to log in</p>
</body>
</html>
Requesting '/secret' without first requesting '/login' will give me HTTP 401 Unauthorized because I can easily check for authentication state in the path handler and respond accordingly. This means my Cookie Authentication setting works.
If I want, I can even configure CookieAuthenticationOptions to automatically replace the HTTP 401 with HTTP redirects to login page, but this is not my current problem.
My problem is, is there a way to also enforce the same authentication for all static contents under wwwroot?
To add, another further question is how to exclude just some static resources. Because obviously from above example, login.html should not be protected by authentication.
We are using .NET Core 3.1 and Google Authentication. This is the code that we have currently:
Startup.cs:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = "CLIENT_ID"
googleOptions.ClientSecret = "CLIENT_SECRET"
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Error/403";
});
AccountController.cs:
public class AccountController : BaseController
{
[AllowAnonymous]
public IActionResult SignInGoogle()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(SignInReturn))
}, GoogleDefaults.AuthenticationScheme);
}
[AllowAnonymous]
public IActionResult SignInReturn()
{
// Do stuff with the user here. Their information is in the User
// property of the controller.
return Ok();
}
}
When users visit /Account/SignInGoogle, they are redirected to Google sign in page. Once they log in successfully, they are redirected back to /Account/SignInReturn. If I place a breakpoint there, I can see that claims are set inside User property.
However, we don't want the User property to be automatically set. We also don't want that the user is considered as logged-in once SignInReturn is called. We would just like to receive information about the user (name, surname, email) and then proceed with our custom claims handling logic. Is it possible?
Google auth uses the OAuth2 protocol. The Google Authentication package just wraps OAuth in an AuthenticationBuilder setup. By using any OAUth2 library you can authenticate outside of the AspNetCore AuthenticationBuilder and retrieve the JWT.
See also: What is the best OAuth2 C# library?
You can access the tokens by handling the OnCreatingTicket event:
googleOptions.Events.OnCreatingTicket = (context) =>
{
string accessToken = context.AccessToken;
string refreshToken = context.RefreshToken;
// do stuff with them
return Task.CompletedTask;
}
Note that you don't get the refresh token unless you specify googleOptions.AccessType = "offline"; and even then you only get them when you first consent (you can trigger reconsent if you require the refresh token).
Or you can follow the approach set out by Microsoft, which basically saves the tokens in a cookie. You can read about that in the documentation here.
I'm working at a project using Auth0. We wish to use the permission system in the Authorization Extension to set it up.
E.g.
Role Admin:
users:viewAll
users:edit
users:xyz
Role User:
users:editOwn
users:viewOwn
users:ect
And then in the Project if possible use the [Authorize(Policy = "users:kvm")] tag.
However, I cannot find any resources on how to actually use the Authorization Extension from Auth0. I'm at a complete loss, so if anyone could guide me on where to even look for these, I'd be very happy.
you can use the Authorization Extension to create a permission that represents access to each application.
Note: While creating permission “Name” should reflect the client id of the application
Example image below
Then create role that represent each application and make sure the relevant permission is selected.
In this example: The role name is” SampleClientAccess”
Then create the group and link the role that you have created. Add relevant users to the group
final step. Go to Dashboard > Rules > create custom rule and the following code.
function (user, context, callback) {
// Assume that permission for an application is the client_id of the permission then
if (user.permissions.indexOf(context.clientID) === -1 ){
callback(new UnauthorizedError('You are not allowed to access ' + context.clientName + JSON.stringify(user)));
}
callback(null, user, context);
}
Hope this will help you in some way.
I have decided to drop auth0's Authorization and work out a system myself.
Can't wrap my head around the documentation.
The authorization extension is accessible via API
You have to enable the API access and set up a machine to machine communication so that you can hit the endpoints. (like described in the link)
Then use this documentation to manage permissions, roles, groups etc.
Each request has to include a token (JWT) which you have to get beforehand from https://yourdomain.auth0.com/oauth/token via a POST request.
You have to provide four parameters:
grant_type = client_credentials
client_id = {from your auth0 application}
client_secret = {from your auth0 application}
audience=urn:auth0-authz-api
Put the token into the header of each request as "Authorization" : "Bearer #YOURTOKEN#"
You can use any REST client to hit the endpoints. For the start I'd recommend Postman to test the endpoints and check which calls you need. There is a handy collection you can use with some adjustments.
I want to add how I'm using it in both legacy .NET MVC applications and .NET Core 2.0 APIs as I hope it'll save someone a lot of time that I've spent trying to figure this out.
If what you want is just to get the groups, permissions, roles and update user accounts in auth0 then follow the steps in the answer by #StV.
But if you want to check permissions/roles etc in .NET then this is how I've done it:
Add the groups, roles and permissions to either the access or Id token (or both). To do this follow the instructions here
Once you publish the rules from the above config step you have to create another rule yourself in Auth0 to copy the info in to the tokens (this got me for a while). this has to run after the rule published/created by Auth0. Mine looks like this:
function (user, context, callback) {
if(user.app_metadata) {
var namespace = 'https://visionplatform.com/';
context.accessToken[namespace + 'roles'] = user.roles;
context.accessToken[namespace + 'permissions'] = user.permissions;
context.idToken[namespace + 'roles'] = user.roles;
context.idToken[namespace + 'permissions'] = user.permissions;
}
callback(null, user, context);
}
Now if you the user logs in they will have their groups, roles and permissions in their tokens. However keep in mind that ONLY the groups, roles and permissions for the specific client you authenticated against will show (I lost hours to this).
So now you can get/check the permissions in code be decoding the JWT. Here's a few snippets of code how I've done this in a library method (i.e. not an authorize attribute):
First get your TokenValidationPrams
public TokenValidationParameters GetTokenValidationParameter(string domain, string audience)
{
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openIdConfig = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync(CancellationToken.None));
return new TokenValidationParameters
{
ValidIssuer = $"{domain}",
ValidAudiences = new[] { audience },
IssuerSigningKeys = openIdConfig.SigningKeys
};
}
Then decode your JWT to get the claims
private ClaimsPrincipal GetValidatedToken(string token, TokenValidationParameters validationParameters)
{
var handler = new JwtSecurityTokenHandler();
return handler.ValidateToken(token, validationParameters, out var _);
}
Now you can check that claims principle to see if it includes your group, permission or whatever (please note I just check the permission).
public bool ValidateTokenClaimsPermissionExists(string token, string domain, string audience, string permission)
{
var claimsPrincipal = GetValidatedToken(token, _tokenValidationParameters);
var scopePermission = claimsPrincipal.FindFirst(c => c.Type == Constants.PermissionsClaimTypeName && c.Value == permission);
return scopePermission != null;
}
I use the above to make separate calls to check permissions but you could (and probably should) write your own authorize attribute or if you're using .NET Core you can write an AuthorizationHandler middleware to check whatever claims you want as per the documentation here. The one below checks the scopes but you could adapt it to check the permissions as per the above code:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
{
return Task.CompletedTask;
}
// Split the scopes string into an array
var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
// Succeed if the scope array contains the required scope
if (scopes.Any(s => s == requirement.Scope))
context.Succeed(requirement);
return Task.CompletedTask;
}
I'm going to use bits from all of the above to write an authorize attribute for my .NET MVC applications too.
For a simple setup you can set the roles through the Auth0 GUI and use a rule to apply that to the user:
function (user, context, callback) {
// Roles should only be set to verified users.
if (!user.email || !user.email_verified) {
return callback(null, user, context);
}
user.app_metadata = user.app_metadata || {};
const assignedRoles = (context.authorization || {}).roles;
const addRolesToUser = function(user) {
return assignedRoles;
};
const roles = addRolesToUser(user);
user.app_metadata.roles = roles;
auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
.then(function() {
context.idToken['https://schemas.<yourdomain>.com'] = user.app_metadata.roles;
callback(null, user, context);
})
.catch(function (err) {
callback(err);
});
}
Your startup.cs should have something like this:
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", authBuilder => { authBuilder.RequireRole("Administrator"); });
options.AddPolicy("User", authBuilder => { authBuilder.RequireRole("Administrator", "User"); });
}
And in the Controller for example:
[Authorize(Roles = "Administrator, User")]
<<your code>>
After upgrading my ASP.NET Core project to 2.0, attempts to access protected endpoints no longer returns 401, but redirects to an (non-existing) endpoint in an attempt to let the user authenticate.
The desired behaviour is for the application simply to return a 401. Previously I would set AutomaticChallenge = false when configuring authentication, but according to this article the setting is no longer relevant (in fact it doesn't exist anymore).
My authentication is configured like this:
Startup.cs.ConfigureServices():
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.Name = options.CookieName;
o.Cookie.Domain = options.CookieDomain;
o.SlidingExpiration = true;
o.ExpireTimeSpan = options.CookieLifetime;
o.TicketDataFormat = ticketFormat;
o.CookieManager = new CustomChunkingCookieManager();
});
Configure():
app.UseAuthentication();
How can I disable automatic challenge, so that the application returns 401 when the user is not authenticated?
As pointed out by some of the other answers, there is no longer a setting to turn off automatic challenge with cookie authentication. The solution is to override OnRedirectToLogin:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
This may change in the future: https://github.com/aspnet/Security/issues/1394
After some research, I found we can deal with this problem though the bellow approach:
We can add two Authentication scheme both Identity and JWT; and use Identity scheme for authentication and use JWT schema for challenge, JWT will not redirect to any login route while challenge.
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication((cfg =>
{
cfg.DefaultScheme = IdentityConstants.ApplicationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})).AddJwtBearer();
Similiar to #Serverin, setting the OnRedirectToLogin of the Application Cookie worked, but must be done in statement following services.AddIdentity in Startup.cs:ConfigureServices:
services.ConfigureApplicationCookie(options => {
options.Events.OnRedirectToLogin = context => {
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
According to this article:
In 1.x, the AutomaticAuthenticate and AutomaticChallenge properties were intended to be set on a single authentication scheme. There was no good way to enforce this.
In 2.0, these two properties have been removed as flags on the individual AuthenticationOptions instance and have moved into the base AuthenticationOptions class. The properties can be configured in the AddAuthentication method call within the ConfigureServices method of Startup.cs
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
Alternatively, use an overloaded version of the AddAuthentication method to set more than one property. In the following overloaded method example, the default scheme is set to CookieAuthenticationDefaults.AuthenticationScheme. The authentication scheme may alternatively be specified within your individual [Authorize] attributes or authorization policies.
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
Define a default scheme in 2.0 if one of the following conditions is true:
You want the user to be automatically signed in
You use the [Authorize] attribute or authorization policies without specifying
schemes
An exception to this rule is the AddIdentity method. This method adds cookies for you and sets the default authenticate and challenge schemes to the application cookie IdentityConstants.ApplicationScheme. Additionally, it sets the default sign-in scheme to the external cookie IdentityConstants.ExternalScheme.
Hope this help you.
This is the source code of CookieAuthenticationEvents.OnRedirectToLogin :
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
You can add "X-Requested-With: XMLHttpRequest" Header to the request while making API calls from your client.
I found that in most cases the solution is to override
OnRedirectToLogin
But in my app I was using multiple authentication policies and overriding of the OnRedirectToLogin did not work for me. The solution in my case it was to add a simple middleware to redirect the incoming request.
app.Use(async (HttpContext context, Func<Task> next) => {
await next.Invoke(); //execute the request pipeline
if (context.Response.StatusCode == StatusCodes.Status302Found && context.Response.Headers.TryGetValue("Location", out var redirect)) {
var v = redirect.ToString();
if (v.StartsWith($"{context.Request.Scheme}://{context.Request.Host}/Account/Login")) {
context.Response.Headers["Location"] = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
context.Response.StatusCode = 401;
}
}
});
Another way to do this which is more DI/testing-friendly is to use AuthenticationSchemeOptions.EventsType (another answer points at it here). This will allow you to pull other components into the resolution process.
Here's an example including registration and resolution which stops the default redirect to login on an unauthenticated request, and instead just returns with a hard 401. It also has a slot for any other dependencies which may need to know about unauthenticated requests.
In Startup.cs:
services
.AddAuthentication("MyAuthScheme")
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.EventsType = typeof(MyEventsWrapper);
};
...
services.AddTransient<MyEventsWrapper>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Then, in MyEventsWrapper.cs:
public class MyEventsWrapper : CookieAuthenticationEvents
{
private readonly IHttpContextAccessor _accessor;
private readonly IDependency _otherDependency;
public MyEventsWrapper(IHttpContextAccessor accessor,
IDependency otherDependency)
{
_accessor = accessor;
_otherDependency = otherDependency;
}
public override async Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
context.Response.Headers.Remove("Location");
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await _otherDependency.Cleanup(_accessor.HttpContext);
}
}
I'm not sure how to generate the 401 error, however if you use the:
o.AccessDeniedPath = "{path to invalid}";
This will allow you to redirect somewhere when the challenge has failed.
I have a custom route that reads URLs from a no-SQL database (MongoDB) and add them to the route pipeline at moment that the application starts, which is "pretty standard"
something like this (in my startup.cs file):
app.UseMvc(routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
routes.Routes.Add(
new LandingPageRouter(routes, webrequest, memoryCache, Configuration));
// this custom routes adds URLs from database
}
the issue is that if I add another route to the database after the application has started I basically get a 404 since the routing system isn't aware of this new route, I think that what I need is add the missing routes at runtime or (less convenient) restart the application pool from another web application (which has been developed on framework 4.5 and obviously it runs on a different pool)
Any other thoughts on this?
thanks.
The first question is what does database mean when you say: I add another route to the database and wether you can keep your routes in a JSON, XML or INI file.
If you can, for example, keep the routes in a JSON file, then there is possible for the routes to be dynamically available on runtime (as you can read in the ASP.NET Core Documentation)
You can find a full blog post about this here.
Assuming that routes.json is a JSON file with structure similar to the following, and is in the same folder as Startup:
{
"route": "This is a route",
"another-route": "This is another route",
"default": "This is default!"
}
You can configure the Startup class in the following way:
Note that this example does not use MVC but the separate routing package, although I assume you can transpose it to work with MVC.
public class Startup
{
public IConfiguration Configuration {get;set;}
public Startup(IHostingEnvironment env)
{
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("routes.json", optional: false, reloadOnChange: true);
Configuration = configurationBuilder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
public void Configure(IApplicationBuilder app)
{
var routeBuilder = new RouteBuilder(app);
routeBuilder.MapGet("{route}", context =>
{
var routeMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == context.GetRouteValue("route")
.ToString())
.Value;
var defaultMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == "default")
.Value;
var response = (routeMessage != null) ? routeMessage : defaultMessage;
return context.Response.WriteAsync(response);
});
app.UseRouter(routeBuilder.Build());
}
}
At this point, while the application is running, you can modify the JSON file, save it, then because of the reloadOnChange: true parameter, the framework will reinject the new configuration into the Configuration property.
The implementation of the reload is based on a file watcher, so if you want to use a database for this - a SQL Server, then I think you have to implement this yourself.
A (not pretty at all and reminded here just for completeness) solution could be to create a console application that adds database changes in a file, then use that file in the application configuration.
Best regards!
In ASP.NET Core 3 you can use Dynamic Routing. In Startup.cs add:
app.UseEndpoints(endpoints =>
{
endpoints.MapDynamicControllerRoute<SearchValueTransformer>("{**url}");
});
And create new class SearchValueTransformer
class SearchValueTransformer : DynamicRouteValueTransformer
{
public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
{
var url = values["url"] as string;
if (String.IsNullOrEmpty(url))
return values;
values["controller"] = "Controller";
values["action"] = "Action";
values["name"] = url;
return values;
}
}
Also in method TransformAsync you can search in your MongoDB for proper Controller, Action and Name values. More info: https://weblogs.asp.net/ricardoperes/dynamic-routing-in-asp-net-core-3