Allow multiple roles to access controller action - asp.net-mvc

Right now I decorate a method like this to allow "members" to access my controller action
[Authorize(Roles="members")]
How do I allow more than one role? For example the following does not work but it shows what I am trying to do (allow "members" and "admin" access):
[Authorize(Roles="members", "admin")]

Another option is to use a single authorize filter as you posted but remove the inner quotations.
[Authorize(Roles="members,admin")]

If you want use custom roles, you can do this:
CustomRoles class:
public static class CustomRoles
{
public const string Administrator = "Administrador";
public const string User = "Usuario";
}
Usage
[Authorize(Roles = CustomRoles.Administrator +","+ CustomRoles.User)]
If you have few roles, maybe you can combine them (for clarity) like this:
public static class CustomRoles
{
public const string Administrator = "Administrador";
public const string User = "Usuario";
public const string AdministratorOrUser = Administrator + "," + User;
}
Usage
[Authorize(Roles = CustomRoles.AdministratorOrUser)]

One possible simplification would be to subclass AuthorizeAttribute:
public class RolesAttribute : AuthorizeAttribute
{
public RolesAttribute(params string[] roles)
{
Roles = String.Join(",", roles);
}
}
Usage:
[Roles("members", "admin")]
Semantically it is the same as Jim Schmehil's answer.

For MVC4, using a Enum (UserRoles) with my roles, I use a custom AuthorizeAttribute.
On my controlled action, I do:
[CustomAuthorize(UserRoles.Admin, UserRoles.User)]
public ActionResult ChangePassword()
{
return View();
}
And I use a custom AuthorizeAttribute like that:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class CustomAuthorize : AuthorizeAttribute
{
private string[] UserProfilesRequired { get; set; }
public CustomAuthorize(params object[] userProfilesRequired)
{
if (userProfilesRequired.Any(p => p.GetType().BaseType != typeof(Enum)))
throw new ArgumentException("userProfilesRequired");
this.UserProfilesRequired = userProfilesRequired.Select(p => Enum.GetName(p.GetType(), p)).ToArray();
}
public override void OnAuthorization(AuthorizationContext context)
{
bool authorized = false;
foreach (var role in this.UserProfilesRequired)
if (HttpContext.Current.User.IsInRole(role))
{
authorized = true;
break;
}
if (!authorized)
{
var url = new UrlHelper(context.RequestContext);
var logonUrl = url.Action("Http", "Error", new { Id = 401, Area = "" });
context.Result = new RedirectResult(logonUrl);
return;
}
}
}
This is part of modifed FNHMVC by Fabricio Martínez Tamayo https://github.com/fabriciomrtnz/FNHMVC/

You can use Authorization Policy
in Startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("admin", policy => policy.RequireRole("SuperAdmin","Admin"));
options.AddPolicy("teacher", policy => policy.RequireRole("SuperAdmin", "Admin", "Teacher"));
});
And in Controller Files:
[Authorize(Policy = "teacher")]
[HttpGet("stats/{id}")]
public async Task<IActionResult> getStudentStats(int id)
{ ... }
"teacher" policy accept 3 roles.

Using AspNetCore 2.x, you have to go a little different way:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class AuthorizeRoleAttribute : AuthorizeAttribute
{
public AuthorizeRoleAttribute(params YourEnum[] roles)
{
Policy = string.Join(",", roles.Select(r => r.GetDescription()));
}
}
just use it like this:
[Authorize(YourEnum.Role1, YourEnum.Role2)]

I mixed answers and proposed this method.
Firstly, We create an enum for role accesses.
public enum ERoleAccess
{
[Description("Admin User")]
Admin = 1,
[Description("General User")]
User = 2,
[Description("Editor User")]
Editor = 3,
}
Secondly, we need an attribute filter for customer MVC authorize.
public class RolesAttribute:AuthorizeAttribute
{
public RolesAttribute(params ERoleAccess[] roles)
{
Roles = string.Join(",", roles);
}
}
Finally, we can use "RolesAttribute" on the controllers or actions.
[Roles(ERoleAccess.Admin, ERoleAccess.Editor, ERoleAccess.User)]
In this approach, we use numbers of alternative string values.
(1= Admin, 2=User,...)
It's good for decreasing token size and comparing performance.

Another clear solution, you can use constants to keep convention and add multiple [Authorize] attributes. Check this out:
public static class RolesConvention
{
public const string Administrator = "Administrator";
public const string Guest = "Guest";
}
Then in the controller:
[Authorize(Roles = RolesConvention.Administrator )]
[Authorize(Roles = RolesConvention.Guest)]
[Produces("application/json")]
[Route("api/[controller]")]
public class MyController : Controller

Better code with adding a subclass AuthorizeRole.cs
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
class AuthorizeRoleAttribute : AuthorizeAttribute
{
public AuthorizeRoleAttribute(params Rolenames[] roles)
{
this.Roles = string.Join(",", roles.Select(r => Enum.GetName(r.GetType(), r)));
}
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary {
{ "action", "Unauthorized" },
{ "controller", "Home" },
{ "area", "" }
}
);
//base.HandleUnauthorizedRequest(filterContext);
}
else
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary {
{ "action", "Login" },
{ "controller", "Account" },
{ "area", "" },
{ "returnUrl", HttpContext.Current.Request.Url }
}
);
}
}
}
How to use this
[AuthorizeRole(Rolenames.Admin,Rolenames.Member)]
public ActionResult Index()
{
return View();
}

If you find yourself applying those 2 roles often you can wrap them in their own Authorize. This is really an extension of the accepted answer.
using System.Web.Mvc;
public class AuthorizeAdminOrMember : AuthorizeAttribute
{
public AuthorizeAdminOrMember()
{
Roles = "members, admin";
}
}
And then apply your new authorize to the Action. I think this looks cleaner and reads easily.
public class MyController : Controller
{
[AuthorizeAdminOrMember]
public ActionResult MyAction()
{
return null;
}
}

[Authorize(Roles="admin")]
[Authorize(Roles="members")]
will work when the AND is needed (as asked by question) whereas this answer shows the OR version.
See more at https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-6.0#adding-role-checks

Intent promptInstall = new Intent(android.content.Intent.ACTION_VIEW);
promptInstall.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
promptInstall.setDataAndType(Uri.parse("http://10.0.2.2:8081/MyAPPStore/apk/Teflouki.apk"), "application/vnd.android.package-archive" );
startActivity(promptInstall);

Related

Is there a way to have a RoutePrefix that starts with an optional parameter?

I want to reach the Bikes controller with these URL's:
/bikes // (default path for US)
/ca/bikes // (path for Canada)
One way of achieving that is using multiple Route Attributes per Action:
[Route("bikes")]
[Route("{country}/bikes")]
public ActionResult Index()
To keep it DRY I'd prefer to use a RoutePrefix, but multiple Route Prefixes are not allowed:
[RoutePrefix("bikes")]
[RoutePrefix("{country}/bikes")] // <-- Error: Duplicate 'RoutePrefix' attribute
public class BikesController : BaseController
[Route("")]
public ActionResult Index()
I've tried using just this Route Prefix:
[RoutePrefix("{country}/bikes")]
public class BikesController : BaseController
Result: /ca/bikes works, /bikes 404s.
I've tried making country optional:
[RoutePrefix("{country?}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
I've tried giving country a default value:
[RoutePrefix("{country=us}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
Is there another way to achieve my objective using Attribute Routing?
(And yes, I know I can do this stuff by registering routes in RouteConfig.cs, but that's what not I'm looking for here).
I'm using Microsoft.AspNet.Mvc 5.2.2.
FYI: these are simplified examples - the actual code has an IRouteConstraint for the {country} values, like:
[Route("{country:countrycode}/bikes")]
I am a bit late to the party, but i have a working solution for this problem. Please find my detailed blog post on this issue here
I am writing down summary below
You need to create 2 files as given below
_3bTechTalkMultiplePrefixDirectRouteProvider.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Web.Http.Controllers;
using System.Web.Http.Routing;
namespace _3bTechTalk.MultipleRoutePrefixAttributes {
public class _3bTechTalkMultiplePrefixDirectRouteProvider: DefaultDirectRouteProvider {
protected override IReadOnlyList GetActionDirectRoutes(HttpActionDescriptor actionDescriptor, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(actionDescriptor.ControllerDescriptor), factories, new [] {
actionDescriptor
}, constraintResolver, true);
}
protected override IReadOnlyList GetControllerDirectRoutes(HttpControllerDescriptor controllerDescriptor, IReadOnlyList actionDescriptors, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(controllerDescriptor), factories, actionDescriptors, constraintResolver, false);
}
private IEnumerable GetRoutePrefixes(HttpControllerDescriptor controllerDescriptor) {
Collection attributes = controllerDescriptor.GetCustomAttributes (false);
if (attributes == null)
return new string[] {
null
};
var prefixes = new List ();
foreach(var attribute in attributes) {
if (attribute == null)
continue;
string prefix = attribute.Prefix;
if (prefix == null)
throw new InvalidOperationException("Prefix can not be null. Controller: " + controllerDescriptor.ControllerType.FullName);
if (prefix.EndsWith("/", StringComparison.Ordinal))
throw new InvalidOperationException("Invalid prefix" + prefix + " in " + controllerDescriptor.ControllerName);
prefixes.Add(prefix);
}
if (prefixes.Count == 0)
prefixes.Add(null);
return prefixes;
}
private IReadOnlyList CreateRouteEntries(IEnumerable prefixes, IReadOnlyCollection factories, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
var entries = new List ();
foreach(var prefix in prefixes) {
foreach(IDirectRouteFactory factory in factories) {
RouteEntry entry = CreateRouteEntry(prefix, factory, actions, constraintResolver, targetIsAction);
entries.Add(entry);
}
}
return entries;
}
private static RouteEntry CreateRouteEntry(string prefix, IDirectRouteFactory factory, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
DirectRouteFactoryContext context = new DirectRouteFactoryContext(prefix, actions, constraintResolver, targetIsAction);
RouteEntry entry = factory.CreateRoute(context);
ValidateRouteEntry(entry);
return entry;
}
private static void ValidateRouteEntry(RouteEntry routeEntry) {
if (routeEntry == null)
throw new ArgumentNullException("routeEntry");
var route = routeEntry.Route;
if (route.Handler != null)
throw new InvalidOperationException("Direct route handler is not supported");
}
}
}
3bTechTalkRoutePrefix.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
namespace _3bTechTalk.MultipleRoutePrefixAttributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class _3bTechTalkRoutePrefix : RoutePrefixAttribute
{
public int Order { get; set; }
public _3bTechTalkRoutePrefix(string prefix) : this(prefix, 0) { }
public _3bTechTalkRoutePrefix(string prefix, int order) : base(prefix)
{
Order = order;
}
}
}
Once done, open WebApiConfig.cs and add this below given line
config.MapHttpAttributeRoutes(new _3bTechTalkMultiplePrefixDirectRouteProvider());
That's it, now you can add multiple route prefix in your controller. Example below
[_3bTechTalkRoutePrefix("api/Car", Order = 1)]
[_3bTechTalkRoutePrefix("{CountryCode}/api/Car", Order = 2)]
public class CarController: ApiController {
[Route("Get")]
public IHttpActionResult Get() {
return Ok(new {
Id = 1, Name = "Honda Accord"
});
}
}
I have uploaded a working solution here
Happy Coding :)
You're correct that you can't have multiple route prefixes, which means solving this particular use case is not going to be straight forward. About the best way I can think of to achieve what you want with the minimal amount of modifications to your project is to subclass your controller. For example:
[RoutePrefix("bikes")]
public class BikeController : Controller
{
...
}
[RoutePrefix("{country}/bikes")]
public class CountryBikeController : BikeController
{
}
You subclassed controller will inherit all the actions from BikeController, so you don't need to redefine anything, per se. However, when it comes to generating URLs and getting them to go to the right place, you'll either need to be explicit with the controller name:
#Url.Action("Index", "CountryBike", new { country = "us" }
Or, if you're using named routes, you'll have to override your actions in your subclassed controller so you can apply new route names:
[Route("", Name = "CountryBikeIndex")]
public override ActionResult Index()
{
base.Index();
}
Also, bear in mind, that when using parameters in route prefixes, all of your actions in that controller should take the parameter:
public ActionResult Index(string country = "us")
{
...
You could use attribute routes with two ordered options.
public partial class GlossaryController : Controller {
[Route("~/glossary", Order = 2)]
[Route("~/{countryCode}/glossary", Order = 1)]
public virtual ActionResult Index()
{
return View();
}
}
If you're planning to have region specific routes for all your pages you could add a route to the route config above the default. This will work only for views/controllers without attribute routes.
routes.MapRoute(
name: "Region",
url: "{countryCode}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { countryCode = #"\w{2}" }
);
The best solution I've come across is detailed by NightOwl888 in response to the following question: ASP.NET MVC 5 culture in route and url. The code below is my trimmed down version of his post. It's working for me in MVC5.
Decorate each controller with a single RoutePrefix, without a culture segment. When the application starts up, the custom MapLocalizedMvcAttributeRoutes method adds a localized route entry for each controller action.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
// Omitted for brevity
MapLocalizedMvcAttributeRoutes(routes, "{culture}/", new { culture = "[a-z]{2}-[A-Z]{2}" });
}
static void MapLocalizedMvcAttributeRoutes(RouteCollection routes, string urlPrefix, object constraints)
{
var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
MethodInfo addMethodInfo = subRouteCollectionType.GetMethod("Add");
var localizedRouteTable = new RouteCollection();
var subRoutes = Activator.CreateInstance(subRouteCollectionType);
Func<Route, RouteBase> createLinkGenerationRoute = (Route route) => (RouteBase)Activator.CreateInstance(linkGenerationRouteType, route);
localizedRouteTable.MapMvcAttributeRoutes();
foreach (var routeCollectionRoute in localizedRouteTable.Where(rb => rb.GetType().Equals(routeCollectionRouteType)))
{
// routeCollectionRoute._subRoutes.Entries
foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(subRoutesInfo.GetValue(routeCollectionRoute)))
{
var localizedRoute = CreateLocalizedRoute(routeEntry.Route, urlPrefix, constraints);
var localizedRouteEntry = new RouteEntry(string.IsNullOrEmpty(routeEntry.Name) ? null : $"{routeEntry.Name}_Localized", localizedRoute);
// Add localized and default routes and subroute entries
addMethodInfo.Invoke(subRoutes, new[] { localizedRouteEntry });
addMethodInfo.Invoke(subRoutes, new[] { routeEntry });
routes.Add(createLinkGenerationRoute(localizedRoute));
routes.Add(createLinkGenerationRoute(routeEntry.Route));
}
}
var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
routes.Add((RouteBase)routeEntries);
}
static Route CreateLocalizedRoute(Route route, string urlPrefix, object constraints)
{
var routeUrl = urlPrefix + route.Url;
var routeConstraints = new RouteValueDictionary(constraints);
// combine with any existing constraints
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
}

How to check in Razor view if ApplicationUser.Roles contains certain role?

I have 3 roles in my webapp: Admin,Moderator,User.
I have a user #model WebApplication2.Models.ApplicationUser I want to check inside Razor view if user.Roles contains role Moderator. How to do so? I tried #if(#Model.Roles.Contains(DON'T_KNOW_WHAT_TO_WRITE_HERE){}.
NOTE: I am not asking how to check if currently authorized user is in certain role.
What you could do is create an extension method on IPrincipal that operates the same way as User.IsInRole(...)
public static bool IsInAppRole(this IPrincipal user, string role)
{
using(var db = new MyEntities())
{
var dbUser = db.Users.Find(user.Identity.GetUserId());
return dbUser.Roles.Any(r => r.RoleName == role)
}
}
Import extensions into a view
#using MyApplication.Web.Extensions
Use like you would IsInRole()
#if(User.IsInAppRole("Admin"))
{
}
Though, not sure why you'd do this as the user's roles can be put into their Identity object.
The simplest way that I could find is to use:
#Model.Roles.SingleOrDefault().RoleId
This, of course, requires you to work with the ID rather than the name in your comparison. Without creating a new ViewModel, I have not found a good way to get direct access to the name.
EDIT: If multiple roles are assigned, you should be able to do something like this:
#{
var isMod = false;
}
foreach (var r in Model.Roles)
{
if(r.RoleId == "1")
{
isMod = true;
}
}
I think you should use User.IsInRole(...) in your view code
Why don't you just print them out to preview possible values?
Your code seems to have minor bug to me. I believe it should be
#if(Model.Roles.Contains(DON'T_KNOW_WHAT_TO_WRITE_HERE){}
(just one '#')
EDIT:
Since Model.Roles are just plain Many-To-Many references, you need to call UserManager to obtain user roles. For example:
public class UserDetailsModel {
public string Id { get; set; }
public ApplicationUser User { get; set; }
public IList<string> Roles { get; set; }
}
and Details action in controller:
public ActionResult Details(string id) {
var model = new UserDetailsModel
{
Id = id,
User = UserManager.FindById(id),
Roles = UserManager.GetRoles(id)
};
return View(model);
}
You can get UserManager from OwinContext or inject it in controller:
private readonly ApplicationUserManager _userManager;
public UsersController(){}
public UsersController(ApplicationUserManager userManager) {
_userManager = userManager;
}
public ApplicationUserManager UserManager {
get {
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
}

How to Disable Attribute on Action When Applied at Controller Level?

I have the following controller:
[NoCache]
public class AccountController : Controller
{
[Authorize(Roles = "admin,useradmin")]
public ActionResult GetUserEntitlementReport(int? userId = null)
{
var byteArray = GenerateUserEntitlementReportWorkbook(allResults);
return File(byteArray,
System.Net.Mime.MediaTypeNames.Application.Octet,
"UserEntitlementReport.xls");
}
}
public class NoCache : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var response = filterContext.HttpContext.Response;
response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
response.Cache.SetValidUntilExpires(false);
response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetNoStore();
base.OnResultExecuting(filterContext);
}
}
As you can see, the controller is decorated with the [NoCache] attribute.
Is there any way to prevent this attribute from being applied to the GetUserEntitlementReport action?
I know I can remove the attribute at the controller level but that is not my favorite solution because the controller contains many other actions and i don't want to have to apply the attribute to each action independently.
You could create a new Attribute which can be used on individual Actions to opt out of the NoCache specified at the controller level.
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public sealed class AllowCache : Attribute { }
Mark any Actions where you want to allow caching with [AllowCache]
Then in the code for your NoCacheAttribute only disable caching if the AllowCache attribute is not present
public class NoCacheAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var actionDescriptor = filterContext.ActionDescriptor;
bool allowCaching = actionDescriptor.IsDefined(typeof(AllowCache), true) ||
actionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowCache), true);
if(!allowCaching)
{
//disable caching.
}
}
}
There is a attribute you can apply on action.
That is [OverrideActionFilters].
[NoCache]
public class AccountController : Controller
{
[Authorize(Roles = "admin,useradmin")]
[OverrideActionFilters]
public ActionResult GetUserEntitlementReport(int? userId = null)
{
var byteArray = GenerateUserEntitlementReportWorkbook(allResults);
return File(byteArray,
System.Net.Mime.MediaTypeNames.Application.Octet,
"UserEntitlementReport.xls");
}
}
This can be accomplished by implementing and registering a custom filter provider. As a starting point, I would derive from FilterAttributeFilterProvider and override GetFilters to remove the NoCache attribute when appropriate.

asp.net mvc decorate [Authorize()] with multiple enums

I have a controller and I want two roles to be able to access it. 1-admin OR 2-moderator
I know you can do [Authorize(Roles="admin, moderators")] but I have my roles in an enum. With the enum I can only authorize ONE role. I can't figure out how to authorize two.
I have tried something like [Authorize(Roles=MyEnum.Admin, MyEnum.Moderator)] but that wont compile.
Someone once suggested this:
[Authorize(Roles=MyEnum.Admin)]
[Authorize(MyEnum.Moderator)]
public ActionResult myAction()
{
}
but it doesn't work as an OR. I think in this case the user has to be part of BOTH roles. Am I overlooking some syntax? Or is this a case where I have to roll my own custom authorization?
Here is a simple and elegant solution which allows you to simply use the following syntax:
[AuthorizeRoles(MyEnum.Admin, MyEnum.Moderator)]
When creating your own attribute, use the params keyword in your constructor:
public class AuthorizeRoles : AuthorizeAttribute
{
public AuthorizeRoles(params MyEnum[] roles)
{
...
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
...
}
}
This will allow you to use the attribute as follows:
[AuthorizeRoles(MyEnum.Admin, MyEnum.Moderator)]
public ActionResult myAction()
{
}
Try using the bit OR operator like this:
[Authorize(Roles= MyEnum.Admin | MyEnum.Moderator)]
public ActionResult myAction()
{
}
If that doesn't work, you could just roll your own. I currently just did this on my project. Here's what I did:
public class AuthWhereRole : AuthorizeAttribute
{
/// <summary>
/// Add the allowed roles to this property.
/// </summary>
public UserRole Is;
/// <summary>
/// Checks to see if the user is authenticated and has the
/// correct role to access a particular view.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
// Make sure the user is authenticated.
if (!httpContext.User.Identity.IsAuthenticated)
return false;
UserRole role = someUser.Role; // Load the user's role here
// Perform a bitwise operation to see if the user's role
// is in the passed in role values.
if (Is != 0 && ((Is & role) != role))
return false;
return true;
}
}
// Example Use
[AuthWhereRole(Is=MyEnum.Admin|MyEnum.Newbie)]
public ActionResult Test() {}
Also, make sure to add a flags attribute to your enum and make sure they are all valued from 1 and up. Like this:
[Flags]
public enum Roles
{
Admin = 1,
Moderator = 1 << 1,
Newbie = 1 << 2
etc...
}
The left bit shifting gives the values 1, 2, 4, 8, 16 and so on.
Well, I hope this helps a little.
I combined a few of the solutions here to create my personal favorite. My custom attribute just changes the data to be in the form that SimpleMembership expects and lets it handle everything else.
My roles enum:
public enum MyRoles
{
Admin,
User,
}
To create roles:
public static void CreateDefaultRoles()
{
foreach (var role in Enum.GetNames(typeof(MyRoles)))
{
if (!Roles.RoleExists(role))
{
Roles.CreateRole(role);
}
}
}
Custom attribute:
public class AuthorizeRolesAttribute : AuthorizeAttribute
{
public AuthorizeRolesAttribute(params MyRoles[] allowedRoles)
{
var allowedRolesAsStrings = allowedRoles.Select(x => Enum.GetName(typeof(MyRoles), x));
Roles = string.Join(",", allowedRolesAsStrings);
}
}
Used like so:
[AuthorizeRoles(MyRoles.Admin, MyRoles.User)]
public ActionResult MyAction()
{
return View();
}
Try
public class CustomAuthorize : AuthorizeAttribute
{
public enum Role
{
DomainName_My_Group_Name,
DomainName_My_Other_Group_Name
}
public CustomAuthorize(params Role[] DomainRoles)
{
foreach (var domainRole in DomainRoles)
{
var domain = domainRole.ToString().Split('_')[0] + "_";
var role = domainRole.ToString().Replace(domain, "").Replace("_", " ");
domain=domain.Replace("_", "\\");
Roles += ", " + domain + role;
}
Roles = Roles.Substring(2);
}
}
public class HomeController : Controller
{
[CustomAuthorize(Role.DomainName_My_Group_Name, Role.DomainName_My_Other_Group_Name)]
public ActionResult Index()
{
return View();
}
}
Here's my version, based on #CalebHC and #Lee Harold's answers.
I've followed the style of using named parameters in the attribute and overridden the base classes Roles property.
#CalebHC's answer uses a new Is property which I think is unnecessary, because AuthorizeCore() is overridden (which in the base class uses Roles) so it makes sense to use our own Roles as well. By using our own Roles we get to write Roles = Roles.Admin on the controller, which follows the style of other .Net attributes.
I've used two constructors to CustomAuthorizeAttribute to show real active directory group names being passed in. In production I use the parameterised constructor to avoid magic strings in the class: group names are pulled from web.config during Application_Start() and passed in on creation using a DI tool.
You'll need a NotAuthorized.cshtml or similar in your Views\Shared folder or unauthorized users will get an error screen.
Here is the code for the base class AuthorizationAttribute.cs.
Controller:
public ActionResult Index()
{
return this.View();
}
[CustomAuthorize(Roles = Roles.Admin)]
public ActionResult About()
{
return this.View();
}
CustomAuthorizeAttribute:
// The left bit shifting gives the values 1, 2, 4, 8, 16 and so on.
[Flags]
public enum Roles
{
Admin = 1,
User = 1 << 1
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private readonly string adminGroupName;
private readonly string userGroupName;
public CustomAuthorizeAttribute() : this("Domain Admins", "Domain Users")
{
}
private CustomAuthorizeAttribute(string adminGroupName, string userGroupName)
{
this.adminGroupName = adminGroupName;
this.userGroupName = userGroupName;
}
/// <summary>
/// Gets or sets the allowed roles.
/// </summary>
public new Roles Roles { get; set; }
/// <summary>
/// Checks to see if the user is authenticated and has the
/// correct role to access a particular view.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>[True] if the user is authenticated and has the correct role</returns>
/// <remarks>
/// This method must be thread-safe since it is called by the thread-safe OnCacheAuthorization() method.
/// </remarks>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
if (!httpContext.User.Identity.IsAuthenticated)
{
return false;
}
var usersRoles = this.GetUsersRoles(httpContext.User);
return this.Roles == 0 || usersRoles.Any(role => (this.Roles & role) == role);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
filterContext.Result = new ViewResult { ViewName = "NotAuthorized" };
}
private IEnumerable<Roles> GetUsersRoles(IPrincipal principal)
{
var roles = new List<Roles>();
if (principal.IsInRole(this.adminGroupName))
{
roles.Add(Roles.Admin);
}
if (principal.IsInRole(this.userGroupName))
{
roles.Add(Roles.User);
}
return roles;
}
}
To add to CalebHC's code and answer ssmith's question about handling users who have multiple roles...
Our custom security principal returns a string array representing all the groups/roles that a user is in. So first we have to convert all the strings in the array that match items in the enum. Finally, we look for any match - if so, then the user is authorized.
Note that we're also redirecting an unauthorized user to a custom "NotAuthorized" view.
The whole class looks like this:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Add the allowed roles to this property.
/// </summary>
public Roles Is { get; set; }
/// <summary>
/// Checks to see if the user is authenticated and has the
/// correct role to access a particular view.
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
throw new ArgumentNullException("httpContext");
if (!httpContext.User.Identity.IsAuthenticated)
return false;
var iCustomPrincipal = (ICustomPrincipal) httpContext.User;
var roles = iCustomPrincipal.CustomIdentity
.GetGroups()
.Select(s => Enum.Parse(typeof (Roles), s))
.ToArray();
if (Is != 0 && !roles.Cast<Roles>().Any(role => ((Is & role) == role)))
{
return false;
}
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
filterContext.Result = new ViewResult { ViewName = "NotAuthorized" };
}
}
Or you could concatenate like:
[Authorize(Roles = Common.Lookup.Item.SecurityRole.Administrator + "," + Common.Lookup.Item.SecurityRole.Intake)]

MVC Authorization - multiple login pages

I have a the following methods in an MVC Controller which redirect to the login page when a user is not logged in.
[Authorize]
public ActionResult Search() {
return View();
}
[Authorize]
public ActionResult Edit() {
return View();
}
Is there a quick/easy/standard way to redirect the second action to a different login page other than the page defined in the web.config file?
Or do I have to do something like
public ActionResult Edit() {
if (IsUserLoggedIn)
return View();
else
return ReturnRedirect("/Login2");
}
I think it is possible by creating a custom authorization filter:
public class CustomAuthorization : AuthorizeAttribute
{
public string LoginPage { get; set; }
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.HttpContext.Response.Redirect(LoginPage);
}
base.OnAuthorization(filterContext);
}
}
In your action:
[CustomAuthorization(LoginPage="~/Home/Login1")]
public ActionResult Search()
{
return View();
}
[CustomAuthorization(LoginPage="~/Home/Login2")]
public ActionResult Edit()
{
return View();
}
Web.config based forms authentication does not have such a functionality built-in (this applies to both WinForms and MVC). You have to handle it yourself (either through an HttpModule or ActionFilter, the method you mentioned or any other method)
I implemented the accepted answer by user434917 and even though I was being redirected correctly, I was also receiving the error "Server cannot set status after HTTP headers have been sent." in the server log. After searching, I found this post (answer by Mattias Jakobsson) that solved the problem. I combined the answers to get this solution.
Create a custom authorization filter:
using System.Web.Mvc;
using System.Web.Routing;
namespace SomeNamespace.CustomFilters
{
public class CustomAuthorization : AuthorizeAttribute
{
public string ActionValue { get; set; }
public string AreaValue { get; set; }
public string ControllerValue { get; set; }
public override void OnAuthorization(AuthorizationContext context)
{
base.OnAuthorization(context);
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
var routeValues = new RouteValueDictionary();
routeValues["area"] = AreaValue;
routeValues["controller"] = ControllerValue;
routeValues["action"] = ActionValue;
context.Result = new System.Web.Mvc.RedirectToRouteResult(routeValues);
}
}
}
}
Then on your controller, use the customer attribute.
[CustomAuthorization(ActionValue = "actionName", AreaValue = "areaName", ControllerValue = "controllerName")]
public class SomeControllerController : Controller
{
//DO WHATEVER
}
Yeah pretty easy! Lets say you have 2 different type of users. First typenormal users, the other one is administrators. You would like to make them login from different pages. You also want them to be able to access different ActionResults.
First you have add two different schemes. In these schemes you will define your different login pages and other options you want.
in startup.cs
services.AddAuthentication("UserSceheme").AddCookie("UserScheme", config =>
{
config.LoginPath = "/UsersLogin/Login/";
config.Cookie.Name = "UsersCookie";
});
services.AddAuthentication("AdminScheme").AddCookie("AdminScheme", config =>
{
config.LoginPath = "/AdminLogin/Login/";
config.Cookie.Name = "AdminsCookie";
});
Then you will define two policies. Here I called them UserAccess and AdminAccess Each policy will use the sceheme that I point.
In startup.cs just after schemes add those below.
services.AddAuthorization(options =>
{
options.AddPolicy("UserAccess", policy =>
{
policy.AuthenticationSchemes.Add("UserScheme");
policy.RequireAuthenticatedUser();
});
options.AddPolicy("AdminAccess", policy =>
{
policy.AuthenticationSchemes.Add("AdminScheme");
policy.RequireAuthenticatedUser();
});
});
The last thing we have to do is again telling the scheme we want to use when login.
var userPrincipal = new ClaimsPrincipal(new[] {
new ClaimsIdentity(loginClaims, "ServiceCenter")
});
HttpContext.SignInAsync("AdminScheme",userPrincipal);
Thats it! Now we can use these just like this; This will redirect you to the users login page.
[Authorize(Policy = "UserAccess")]
public IActionResult Index()
{
return View();
}
If you have some places that you want both user types to be able to access all you have to do;
[Authorize(Policy = "AdminAccess")]
[Authorize(Policy = "UserAccess")]
public IActionResult Index()
{
return View();
}
And finally for log-out you also have to point the scheme
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync("AdminScheme");
return View();
}
Thats it!

Resources