In a regular type scenario, where a Route is available, say to only "Premium" users, ocelot.global.json would have RouteClaimsRequirement like this:
"RouteClaimsRequirement" : { "Role" : "Premium" }
This would get translated to a KeyValuePair<string, string>(), and it works nicely.
However, if I were to open a route to 2 types of users, eg. "Regular" and "Premium", how exactly could I achieve this?
I found a way through overriding of default Ocelot middleware. Here are some useful code snippets:
First, override the default AuthorizationMiddleware in Configuration() in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var config = new OcelotPipelineConfiguration
{
AuthorisationMiddleware
= async (downStreamContext, next) =>
await OcelotJwtMiddleware.CreateAuthorizationFilter(downStreamContext, next)
};
app.UseOcelot(config).Wait();
}
As you can see, I am using a custom OcelotJwtMiddleware class up there. Here is that class, pasted:
public static class OcelotJwtMiddleware
{
private static readonly string RoleSeparator = ",";
public static Func<DownstreamContext, Func<Task>, Task> CreateAuthorizationFilter
=> async (downStreamContext, next) =>
{
HttpContext httpContext = downStreamContext.HttpContext;
var token = httpContext.Request.Cookies[JwtManager.AuthorizationTokenKey];
if (token != null && AuthorizeIfValidToken(downStreamContext, token))
{
await next.Invoke();
}
else
{
downStreamContext.DownstreamResponse =
new DownstreamResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
};
private static bool AuthorizeIfValidToken(DownstreamContext downStreamContext, string jwtToken)
{
IIdentityProvider decodedObject = new JwtManager().Decode<UserToken>(jwtToken);
if (decodedObject != null)
{
return downStreamContext.DownstreamReRoute.RouteClaimsRequirement["Role"]
?.Split(RoleSeparator)
.FirstOrDefault(role => role.Trim() == decodedObject.GetRole()) != default;
}
return false;
}
}
JwtManager class here is just my small utility made using the default Jwt NuGet package, nothing special. Also, JWT is being stored as a Cookie, which is not safe, but doesn't matter here. If you happen to copy paste your code, you will have small errors relating to this, but just switch it out with your own implementations of auth tokens.
After these 2 snippets were implemented, ocelot.global.json can have RouteClaimsRequirement such as this:
"RouteClaimsRequirement" : { "Role" : "Premium, Regular" }
This will recognize both clients with Regular in their Cookies, as well as those with Premium.
Related
Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});
In AspNetCore, given a FilterContext, I'm looking to get a route template e.g.
{controller}/{action}/{id?}
In Microsoft.AspNet.WebApi I could get the route template from:
HttpControllerContext.RouteData.Route.RouteTemplate
In System.Web.Mvc I could get this from:
ControllerContext.RouteData.Route as RouteBase
In AspNetCore there is:
FilterContext.ActionDescriptor.AttributeRouteInfo.Template
However, not all routes are attribute routes.
Based on inspection if the attribute is not available, default routes and/or mapped routes can be assembled from:
FilterContext.RouteData.Routers.OfType<Microsoft.AspNetCore.Routing.RouteBase>().First()
but I'm looking for a documented or a simply better approach.
Update (24 Jan 2021)
There is a much much simpler way of retrieving the RoutePattern directly via the HttpContext.
FilterContext filterContext;
var endpoint = filterContext.HttpContext.GetEndpoint() as RouteEndpoint;
var template = endpoint?.RoutePattern?.RawText;
if (template is null)
throw new Exception("No route template found, that's absurd");
Console.WriteLine(template);
GetEndpoint() is an extension method provided in EndpointHttpContextExtensions class inside Microsoft.AspNetCore.Http namespace
Old Answer (Too much work)
All the route builders for an ASP.NET Core app (at least for 3.1) are exposed and registered via IEndpointRouteBuilder, but unfortunately, this is not registered with the DI container, so you can't acquire it directly.The only places where I have seen this interface being exposed, are in the middlewares.
So you can build a collection or dictionary out of one of those middlewares, and then use that for your purposes.
e.g
Program.cs
Extension class to build your endpoint collection / dictionary
internal static class IEndpointRouteBuilderExtensions
{
internal static void BuildMap(this IEndpointRouteBuilder endpoints)
{
foreach (var item in endpoints.DataSources)
foreach (RouteEndpoint endpoint in item.Endpoints)
{
/* This is needed for controllers with overloaded actions
* Use the RoutePattern.Parameters here
* to generate a unique display name for the route
* instead of this list hack
*/
if (Program.RouteTemplateMap.TryGetValue(endpoint.DisplayName, out var overloadedRoutes))
overloadedRoutes.Add(endpoint.RoutePattern.RawText);
else
Program.RouteTemplateMap.Add(endpoint.DisplayName, new List<string>() { endpoint.RoutePattern.RawText });
}
}
}
public class Program
{
internal static readonly Dictionary<string, List<string>> RouteTemplateMap = new Dictionary<string, List<string>>();
/* Rest of things */
}
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
/* all other middlewares */
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
//Use this at the last middlware exposing IEndpointRouteBuilder so that all the routes are built by this point
endpoints.BuildMap();
});
}
And then you can use that Dictionary or Collection, to retrieve the Route Template from the FilterContext.
FilterContext filterContext;
Program.RouteTemplateMap.TryGetValue(filterContext.ActionDescriptor.DisplayName, out var template);
if (template is null)
throw new Exception("No route template found, that's absurd");
/* Use the ActionDescriptor.Parameters here
* to figure out which overloaded action was called exactly */
Console.WriteLine(string.Join('\n', template));
To tackle the case of overloaded actions, a list of strings is used for route template (instead of just a string in the Dictionary)
You can use the ActionDescriptor.Parameters in conjunction with RoutePattern.Parameters to generate a unique display name for that route.
These are the assembled versions, but still looking for a better answer.
AspNetCore 2.0
FilterContext context;
string routeTemplate = context.ActionDescriptor.AttributeRouteInfo?.Template;
if (routeTemplate == null)
{
// manually mapped routes or default routes
// todo is there a better way, not 100% sure that this is correct either
// https://github.com/aspnet/Routing/blob/1b0258ab8fccff1306e350fd036d05c3110bbc8e/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
IEnumerable<string> segments = context.RouteData.Routers.OfType<Microsoft.AspNetCore.Routing.RouteBase>()
.FirstOrDefault()?.ParsedTemplate.Segments.Select(s => string.Join(string.Empty, s.Parts
.Select(p => p.IsParameter ? $"{{{(p.IsCatchAll ? "*" : string.Empty)}{p.Name}{(p.IsOptional ? "?" : string.Empty)}}}" : p.Text)));
if (segments != null)
{
routeTemplate = string.Join("/", segments);
}
}
AspNetCore 3.0 with Endpoint Routing
RoutePattern routePattern = null;
var endpointFeature = context.HttpContext.Features[typeof(Microsoft.AspNetCore.Http.Features.IEndpointFeature)]
as Microsoft.AspNetCore.Http.Features.IEndpointFeature;
var endpoint = endpointFeature?.Endpoint;
if (endpoint != null)
{
routePattern = (endpoint as RouteEndpoint)?.RoutePattern;
}
string formatRoutePart(RoutePatternPart part)
{
if (part.IsParameter)
{
RoutePatternParameterPart p = (RoutePatternParameterPart)part;
return $"{{{(p.IsCatchAll ? "*" : string.Empty)}{p.Name}{(p.IsSeparator ? " ? " : string.Empty)}}}";
}
else if (part.IsLiteral)
{
RoutePatternLiteralPart p = (RoutePatternLiteralPart)part;
return p.Content;
}
else if(part.IsSeparator)
{
RoutePatternSeparatorPart p = (RoutePatternSeparatorPart)part;
return p.Content;
}
else
{
throw new NotSupportedException("Unknown Route PatterPart");
}
}
if (routePattern != null)
{
// https://github.com/aspnet/Routing/blob/1b0258ab8fccff1306e350fd036d05c3110bbc8e/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
routeString = string.Join("/", routePattern.PathSegments.SelectMany(s => s.Parts).Select(p => formatRoutePart(p)));
}
In my current application, I am using Service Stack with JWT's for security. Security has been implemented and works perfectly. Trouble is, I would like to secure one route differently from the others. There is a document the logged in user retrieves, I want to make sure the document they are retrieving is theirs and not someone else's. It is very sensitive data. I would like to secure it differently because something like PostMan could be used with a valid token to retrieve any document, I want to prevent this. The users id is in the token, I would like to match it against the document that is being retrieved if possible. The current security is implemented like so:
public class AppHost: AppHostBase
{
public override void Configure(Funq.Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new JsonWebTokenAuthProvider("myKey", "myAudience"),
}));
}
}
JsonWebTokenAuthProvider is a custom class where security was implemented, this all works perfectly. Here is the code:
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
// first validate the token, then get roles from session
string header = request.oauth_token;
// if no auth header, 401
if (string.IsNullOrEmpty(header))
{
throw HttpError.Unauthorized(MissingAuthHeader);
}
string[] headerData = header.Split(' ');
// if header is missing bearer portion, 401
if (!string.Equals(headerData[0], "BEARER", StringComparison.OrdinalIgnoreCase))
{
throw HttpError.Unauthorized(InvalidAuthHeader);
}
// swap - and _ with their Base64 string equivalents
string secret = SymmetricKey.Replace('-', '+').Replace('_', '/');
string token = headerData[1].Replace("\"", "");
// set current principal to the validated token principal
Thread.CurrentPrincipal = JsonWebToken.ValidateToken(token, secret, Audience, true, Issuer);
string lanId = GetLanID(Thread.CurrentPrincipal.Identity.Name);
string proxyAsLanId = request.Meta.ContainsKey(META_PROXYID) ? request.Meta[META_PROXYID] : null;
if (HttpContext.Current != null)
{
// set the current request's user the the decoded principal
HttpContext.Current.User = Thread.CurrentPrincipal;
}
// set the session's username to the logged in user
session.UserName = Thread.CurrentPrincipal.Identity.Name;
session.Roles = GetApplicableRoles(lanId, proxyAsLanId);
authService.Request.SetItem("lanID", lanId);
authService.Request.SetItem("proxyAsLanId", proxyAsLanId);
return OnAuthenticated(authService, session, null, null);
}
I looked up RequestFilterAttribute found here, but I do not think that is what I want. Ideally, if the check fails I would like to return a 401 (unauthorized) if possible.
What is the best way to do this?
If you just want to handle one route differently than you can just add the validation in your single Service, e.g:
public object Any(MyRequest dto)
{
var lanId = base.Request.GetItem("lanId");
if (!MyIsValid(lanId))
throw HttpError.Unauthorized("Custom Auth Validation failed");
}
You could do the same in a RequestFilter, e.g:
public class CustomAuthValidationAttribute : RequestFilterAttribute
{
public override void Execute(IRequest req, IResponse res, object responseDto)
{
var lanId = req.GetItem("lanId");
if (!MyIsValid(lanId))
{
res.StatusCode = (int) HttpStatusCode.Unauthorized;
res.StatusDescription = "Custom Auth Validation failed";
res.EndRequest();
}
}
}
And apply it to a single Service:
[CustomAuthValidation]
public object Any(MyRequest dto)
{
//...
}
Or a collection of Services, e.g:
[CustomAuthValidation]
public class MyAuthServices : Service
{
public object Any(MyRequest1 dto)
{
//...
}
public object Any(MyRequest2 dto)
{
//...
}
}
I have an action, TopSecret(), which has a security policy applied to it:
[Authorize(Policy = "Level2SecurityClearance")]
public IActionResult TopSecret()
I could check the user meets the requirements of the policy by doing this (authorizationService is of type IAuthorizationService)
bool isAuthorised = await authorizationService.AuthorizeAsync(User, "Level2SecurityClearance");
This action may have a different policy applied at some point in the future and I don't want to have to find all the places I generate links to it and update the code. Is it possible to test if a user can access a specific action?
Maybe something like this:
// Not a real method!!!
bool isAuthorised = authorizationService.IsAuthorisedForAction(User, "TopSecret", "SecretController");
You should look into developing Requirements
Here's an example for you using your criteria:
note: I'm assuming you're using Identity3 and your User has claims with the access
In a new class called Level2SecurityClearanceRequirement
public class Level2SecurityClearanceRequirement : AuthorizationHandler<Level2SecurityClearanceRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Level2SecurityClearanceRequirement requirement)
{
if (context.User.HasClaim("TopSecret","yes")
context.Succeed(requirement);
return Task.FromResult(0);
}
}
In your controller method:
public async Task<IActionResult> BlahBlah() {
if (!await _authorizationService.AuthorizeAsync(User, nameof(PolicyName.Level2SecurityClearance), new Level2SecurityClearanceRequirement()))
return new ChallengeResult();
}
note that I'm using nameof() here so that you don't have any magic strings and all your resources are centralized.
In this case I have an enum:
public enum PolicyName {
Level2SecurityClearance
}
in your startup.cs:
in the ConfigureServices method
add the following:
services.AddAuthorization(options =>{
options.AddPolicy(nameof(PolicyName.Level2SecurityClearance), policy => { policy.AddRequirements(new Level2SecurityClearanceRequirement()); });
});
you can then use this requirement whereever you please and the checks are done in the requirement itself
Try this. Tested in ASP.NET Core 1.1
//somewhere in view
#if (await Url.HasAccess(urlActionContext))
{
<p>You have access</p>
}
Extension method
public static async Task<bool> HasAccess(this IUrlHelper urlHelper, UrlActionContext urlActionContext, string httpMethod = "GET" )
{
var httpContext = urlHelper.ActionContext.HttpContext;
var routeValues = new RouteValueDictionary(urlActionContext.Values);
routeValues["action"] = urlActionContext.Action;
routeValues["controller"] = urlActionContext.Controller;
var path = urlHelper.Action(urlActionContext);
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature()
{
Method = httpMethod,
Path = path,
});
var ctx = new DefaultHttpContext(features);
var routeContext = new RouteContext(ctx);
foreach (var entry in routeValues)
{
routeContext.RouteData.Values.Add(entry.Key, entry.Value);
}
var actionSelector = httpContext.RequestServices.GetRequiredService<IActionSelector>();
var provider = httpContext.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
var actionDescriptors = actionSelector.SelectCandidates(routeContext);
var actionDescriptor = actionSelector.SelectBestCandidate(routeContext, actionDescriptors);
var authService = httpContext.RequestServices.GetRequiredService<IAuthorizationService>();
//You need to implement your own AuthorizationHandler that
//checks the actionDescriptor. It will be in AuthorizationHandlerContext.Resource.
//In my case, I have custom Authorize attribute applied to the
//controller action and this attribute is available
//in actionDescriptor.FilterDescriptors
var ok = await authService.AuthorizeAsync(httpContext.User, actionDescriptor, "YOUR_POLICY");
return ok;
}
From what I saw in the source code RequiresAuthentication() does an Authentication check for the whole module. Is there any way to do this per Route?
I had the same problem. However it turns out the RequiresAuthentication works at both the module level and the route level. To demonstrate, here is some code ripped out my current project (not all routes shown for brevity).
public class RegisterModule : _BaseModule
{
public RegisterModule() : base("/register")
{
Get["/basic-details"] = _ => View["RegisterBasicDetailsView", Model];
Get["/select"] = _ =>
{
this.RequiresAuthentication();
return View["RegisterSelectView", Model];
};
}
}
Of course the only problem with doing it this way is that all the protected routes in the module need to call RequiresAuthentication. In the case of my module above, I have another 5 routes (not shown) all of which need protecting, so that makes six calls to RequiresAuthentication instead of one at the module level. The alternative would be to pull the unprotected route into another module, but my judgement was that a proliferation of modules is worse than the additional RequiresAuthentication calls.
namespace Kallist.Modules {
#region Namespaces
using System;
using Nancy;
#endregion
public static class ModuleExtensions {
#region Methods
public static Response WithAuthentication(this NancyModule module, Func<Response> executeAuthenticated) {
if ((module.Context.CurrentUser != null) && !string.IsNullOrWhiteSpace(module.Context.CurrentUser.UserName)) {
return executeAuthenticated();
}
return new Response { StatusCode = HttpStatusCode.Unauthorized };
}
#endregion
}
}
I ran into the same issue, here's how I solved it.
var module = new MyModule();
module.AddBeforeHookOrExecute(context => null, "Requires Authentication");
_browser = new Browser(with =>
{
with.Module(module);
with.RequestStartup((container, pipelines, ctx) =>
{
ctx.CurrentUser = new User { UserId = "1234", UserName = "test"};
});
});
I can now use this.RequiresAuthentication() at the module level and run my unit tests.