I've set up my menu using MVCSiteMap and I have this node:
<mvcSiteMapNode title="Courses Form" controller="Booking" action="Course" roles="CORLIC, VIEWCOBO"/>
I'm trying to enforce that this node must have roles "CORLIC" AND "VIEWCOBO" for it to be visible but of course this means that it will be displayed if the user has either of the above.
Is this possible?
Thanks.
The roles attribute is for interoperability with ASP.NET and should not be used in an MVC-only application.
For MVC, if you are already defining the AuthorizeAttribute on your controller actions, MvcSiteMapProvider will automatically pick them up and hide the matching nodes accordingly if security trimming is enabled.
[Authorize]
public ActionResult Course()
{
return View();
}
[Authorize]
[HttpPost]
public ActionResult Course(CourseModel model)
{
if (ModelState.IsValid)
{
// Implementation omitted
}
// If we got this far, something failed, redisplay form
return View(model);
}
The default AuthorizeAttribute accepts roles, but it works in the same way as the roles attribute - that is, any role that the user is in will cause it to succeed.
However, you could inherit AuthorizeAttribute yourself and override the IsAuthorized method to change the logic as needed.
public class SpecialAuthorizeAttribute : AuthorizeAttribute
{
private string _requiredRoles;
private string[] _requiredRolesSplit = new string[0];
/// <summary>
/// Gets or sets the required roles. The user must be a member of all roles for it to succeed.
/// </summary>
/// <value>
/// The roles string.
/// </value>
/// <remarks>Multiple role names can be specified using the comma character as a separator.</remarks>
public string RequiredRoles
{
get { return _requiredRoles ?? String.Empty; }
set
{
_requiredRoles = value;
_requiredRolesSplit = SplitString(value);
}
}
/// <summary>
/// Determines whether access for this particular request is authorized. This method uses the user <see cref="IPrincipal"/>
/// returned via <see cref="HttpRequestContext.Principal"/>. Authorization is denied if the user is not authenticated,
/// the user is not in the authorized group of <see cref="Users"/> (if defined), or if the user is not in any of the authorized
/// <see cref="Roles"/> (if defined).
/// </summary>
/// <param name="actionContext">The context.</param>
/// <returns><c>true</c> if access is authorized; otherwise <c>false</c>.</returns>
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException("actionContext");
}
IPrincipal user = actionContext.ControllerContext.RequestContext.Principal;
if (user == null || user.Identity == null || !user.Identity.IsAuthenticated)
{
return false;
}
// Ensure all of the roles in RequiredRoles are present.
if (_requiredRolesSplit.Length > 0 && !_requiredRolesSplit.All(user.IsInRole))
{
return false;
}
// Call the base class to check the users and roles there.
return base.IsAuthorized(actionContext);
}
/// <summary>
/// Splits the string on commas and removes any leading/trailing whitespace from each result item.
/// </summary>
/// <param name="original">The input string.</param>
/// <returns>An array of strings parsed from the input <paramref name="original"/> string.</returns>
internal static string[] SplitString(string original)
{
if (String.IsNullOrEmpty(original))
{
return new string[0];
}
var split = from piece in original.Split(',')
let trimmed = piece.Trim()
where !String.IsNullOrEmpty(trimmed)
select trimmed;
return split.ToArray();
}
}
Then you can specify which roles are required by using the new property.
[SpecialAuthorize(RequiredRoles = "CORLIC, VIEWCOBO")]
public ActionResult Course()
{
return View();
}
[SpecialAuthorize(RequiredRoles = "CORLIC, VIEWCOBO")]
[HttpPost]
public ActionResult Course(CourseModel model)
{
if (ModelState.IsValid)
{
// Implementation omitted
}
// If we got this far, something failed, redisplay form
return View(model);
}
Another possible option is to use FluentSecurity as shown here. For FluentSecurity v2.0 to work with MvcSiteMapProvider, you need to copy the HandleSecurityAttribute code into your project and change it to inherit from AuthorizeAttribute instead of Attribute, then use it as specified in the FluentSecurity documentation.
Related
I have an intranet application where all user operations are conducted by API calls to a remote system (no local tables). A couple of the API calls require the user's password. I can't really ask users to keep reentering their password as they use the site (sometimes seconds after they've just logged in).
So without saving their password to a database, where can I safely cache the password for the duration of the user's login (note: "login", not "session"). I tried storing them in the Session state, but the problem is the session only lasts 20 minutes but the login token is valid for 24 hours.
Ideally I want it linked (somehow) directly to the .AspNet.ApplicationCookie so the login and the cached password cannot get out of sync, but it doesn't see like it's possible to add custom values to that cookie. It can be encrypted if this cookie isn't already encrypted.
EDIT:
Due to the "remember me" function, logins can last much longer than the Session.TimeOut value, so I don't want to use the Session for this.
I had a project where I had to implement exactly the same and ended up with a custom implementation of the ASP.NET Identity interfaces. (In my case the usernames and passwords were managed by an external system with an API.)
I'll explain the idea and main parts of the code.
The required userinfo (eg. username and password) gets stored in memory in a ConcurrentDictionary within a custom IUserStore, by definition the place by which userinfo gets retrieved.
Note; I am going to skip security best practices.
The only place to have access to the password of a user is via the PasswordSignInAsync method of a custom SignInManager.
Here things get different!
In the default/regular flow, the SignInManager uses the IUserStore to retrieve userinfo in order to do the password check. But because the IUserStore's role changed into a passive memory store that isn't possible anymore; this initial lookup must be done via eg. a database lookup.
Then the SignInManager does the password check.
If valid, the userinfo gets added or updated into the custom IUserStore (via a custom method on the CustomUserStore.)
It is important also to do an update every time the user signs in, otherwise the password stays stale, as it is being kept in memory for the duration of the application.
In case the web application gets recycled and the userinfo in the Dictionary gets lost, the ASP.NET identity framework takes care of this by redirecting the user again to the login page, by which the above flow starts again.
Next requirement is a custom UserManager, as my IUserStore does not implement all interfaces required by ASP.NET Identity; see the comments in the code. This may be different for your case.
With all this in place you retrieve a CustomUser via the UserManager; with the user object holding the password:
CustomUser user = this._userManager.FindById(userName);
Here below are some extracts of the implementation.
The data that gets stored in memory:
public class UserInfo
{
String Password { get; set; }
String Id { get; set; }
String UserName { get; set; }
}
The custom IUser:
public class CustomUser : IUser<String>
{
public String Id { get; }
public String Password { get; set; }
public String UserName { get; set; }
}
The custom IUserStore with a method to write to it:
public interface ICustomUserStore : IUserStore<CustomUser>
{
void CreateOrUpdate(UserInfo user);
}
The custom UserStore:
public class CustomUserStore : ICustomUserStore
{
private readonly ConcurrentDictionary<String, CustomUser> _users = new ConcurrentDictionary<String, CustomUser>(StringComparer.OrdinalIgnoreCase);
public Task<CustomUser> FindByIdAsync(String userId)
{
// UserId and userName are being treated as the same.
return this.FindByNameAsync(userId);
}
public Task<CustomUser> FindByNameAsync(String userName)
{
if (!this._users.ContainsKey(userName))
{
return Task.FromResult(null as CustomUser);
}
CustomUser user;
if (!this._users.TryGetValue(userName, out user))
{
return Task.FromResult(null as CustomUser);
}
return Task.FromResult(user);
}
public void CreateOrUpdate(UserInfo userInfo)
{
if (userInfo != null)
{
this._users.AddOrUpdate(userInfo.UserName,
// Add.
key => new CustomUser { Id = userInfo.Id, UserName = userInfo.UserName, Password = userInfo.Password) }
// Update; prevent stale password.
(key, value) => {
value.Password = userInfo.Password;
return value
});
}
}
}
The custom UserManager:
public class CustomUserManager : UserManager<CustomUser>
{
public CustomUserManager(ICustomUserStore userStore)
: base(userStore)
{}
/// Must be overridden because ICustomUserStore does not implement IUserPasswordStore<CustomUser>.
public override Task<Boolean> CheckPasswordAsync(CustomUser user, String password)
{
return Task.FromResult(true);
}
/// Must be overridden because ICustomUserStore does not implement IUserTwoFactorStore<CustomUser>.
public override Task<Boolean> GetTwoFactorEnabledAsync(String userId)
{
return Task.FromResult(false);
}
/// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>.
public override Task<Boolean> IsLockedOutAsync(String userId)
{
return Task.FromResult(false);
}
/// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>.
public override Task<IdentityResult> ResetAccessFailedCountAsync(String userId)
{
Task.FromResult(IdentityResult.Success);
}
}
The custom SignInManager:
public class CustomSignInManager : SignInManager<CustomUser, String>
{
private readonly ICustomUserStore _userStore;
public CustomSignInManager(
CustomUserManager userManager,
IAuthenticationManager authenticationManager
ICustomUserStore userStore
)
: base(userManager, authenticationManager)
{
this._userStore = userStore;
}
/// Provided by the ASP.NET MVC template.
public override Task<ClaimsIdentity> CreateUserIdentityAsync(CustomUser user)
{
return user.GenerateUserIdentityAsync(this.UserManager);
}
public override Task<SignInStatus> PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean shouldLockout)
{
UserInfo userInfo = // Call method the retrieve user info from eg. the database.
if (null == userInfo)
{
return Task.FromResult(SignInStatus.Failure);
}
// Do password check; if not OK:
// return Task.FromResult(SignInStatus.Failure);
// Password is OK; set data to the store.
this._userStore.CreateOrUpdate(userInfo);
// Execute the default flow, which will now use the IUserStore with the user present.
return base.PasswordSignInAsync(userName, password, isPersistent, shouldLockout);
}
}
Disclaimer: Here you are putting a password into a cookie. An encrypted cookie, yet a password. It is not the best practice from security point of view. So make a decision yourself if this is acceptable for your system or not.
I think the best way for this would be to store the password as a claim on the authentication cookie. Auth cookie is encrypted when transmitted but you don't have to deal with the encryption yourself - this is done by OWIN for you. And this requires a lot less plumbing.
First rewrite your login action as follows:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindAsync(model.Email, model.Password);
if (user == null)
{
// user with this username/password not found
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
// BEWARE this does not check if user is disabled, locked or does not have a confirmed user
// I'll leave this for you to implement if needed.
var userIdentity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
userIdentity.AddClaim(new Claim("MyApplication:Password", model.Password));
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, userIdentity);
return RedirectToLocal(returnUrl);
}
This takes password on login and adds it as a claim on the Identity that in turn gets serialised and encrypted into a cookie.
Note that a lot of logic has been omitted here - if you need to check if user is disabled, locked or without a confirmed email, you'll need to add that yourself. I suspect you won't need that as you mentioned that this is an internal only site.
Next you'll need an extension method to extract the password out:
using System;
using System.Security.Claims;
using System.Security.Principal;
public static class PrincipalExtensions
{
public static String GetStoredPassword(this IPrincipal principal)
{
var claimsPrincipal = principal as ClaimsPrincipal;
if (claimsPrincipal == null)
{
throw new Exception("Expecting ClaimsPrincipal");
}
var passwordClaim = claimsPrincipal.FindFirst("MyApplication:Password");
if (passwordClaim == null)
{
throw new Exception("Password is not stored");
}
var password = passwordClaim.Value;
return password;
}
}
That is pretty much it. Now in every action you can apply that method on User property:
[Authorize]
public ActionResult MyPassword()
{
var myPassword = User.GetStoredPassword();
return View((object)myPassword);
}
And corresponding view will be like this:
#model String
<h2>Password is #Model</h2>
However, depending on your requirements this password claim can be killed over time or preserved. Default Identity template enables SecurityStampInvalidator that is executed every 30 minutes on the cookie and rewrites it fresh from the DB. Usually ad-hoc claims added like this do not survive this rewrite.
To preserve the password value past 30 minutes of cookie age take this class:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
// This is mostly copy of original security stamp validator, only with addition to keep hold of password claim
// https://github.com/aspnet/AspNetIdentity/blob/a24b776676f12cf7f0e13944783cf8e379b3ef70/src/Microsoft.AspNet.Identity.Owin/SecurityStampValidator.cs#L1
public class MySecurityStampValidator
{
/// <summary>
/// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security
/// stamp after validateInterval
/// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
/// ClaimsIdentity
/// </summary>
/// <typeparam name="TManager"></typeparam>
/// <typeparam name="TUser"></typeparam>
/// <param name="validateInterval"></param>
/// <param name="regenerateIdentity"></param>
/// <returns></returns>
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser>(
TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentity)
where TManager : UserManager<TUser, string>
where TUser : class, IUser<string>
{
return OnValidateIdentity(validateInterval, regenerateIdentity, id => id.GetUserId());
}
/// <summary>
/// Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security
/// stamp after validateInterval
/// Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
/// ClaimsIdentity
/// </summary>
/// <typeparam name="TManager"></typeparam>
/// <typeparam name="TUser"></typeparam>
/// <typeparam name="TKey"></typeparam>
/// <param name="validateInterval"></param>
/// <param name="regenerateIdentityCallback"></param>
/// <param name="getUserIdCallback"></param>
/// <returns></returns>
public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
Func<ClaimsIdentity, TKey> getUserIdCallback)
where TManager : UserManager<TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
if (getUserIdCallback == null)
{
throw new ArgumentNullException("getUserIdCallback");
}
return async context =>
{
var currentUtc = DateTimeOffset.UtcNow;
if (context.Options != null && context.Options.SystemClock != null)
{
currentUtc = context.Options.SystemClock.UtcNow;
}
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate)
{
var manager = context.OwinContext.GetUserManager<TManager>();
var userId = getUserIdCallback(context.Identity);
if (manager != null && userId != null)
{
var user = await manager.FindByIdAsync(userId);
var reject = true;
// Refresh the identity if the stamp matches, otherwise reject
if (user != null && manager.SupportsUserSecurityStamp)
{
var securityStamp =
context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
if (securityStamp == await manager.GetSecurityStampAsync(userId))
{
reject = false;
// Regenerate fresh claims if possible and resign in
if (regenerateIdentityCallback != null)
{
var identity = await regenerateIdentityCallback.Invoke(manager, user);
if (identity != null)
{
var passwordClaim = context.Identity.FindFirst("MyApplication:Password");
if (passwordClaim != null)
{
identity.AddClaim(passwordClaim);
}
// Fix for regression where this value is not updated
// Setting it to null so that it is refreshed by the cookie middleware
context.Properties.IssuedUtc = null;
context.Properties.ExpiresUtc = null;
context.OwinContext.Authentication.SignIn(context.Properties, identity);
}
}
}
}
if (reject)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
}
};
}
}
Note that this is a direct copy of original Identity code with minor modification to preserve the password claim.
And to activate this class, in your Startup.Auth.cs do this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// use MySecurityStampValidator here
OnValidateIdentity = MySecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(10), // adjust time as required
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
Here is a working sample code
I'm rather surprised at the default behaviour of AuthorizeAttribute; if you don't supply it any Roles property, it just appears to allow any authorized user to access the controller/action. I want whitelist behaviour instead; if Roles is null or empty, deny all users access. How can I make this behaviour occur?
public class AuthorizeExAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (string.IsNullOrWhiteSpace(Roles))
return false;
return base.AuthorizeCore(httpContext);
}
}
Now use [AuthorizeEx] on your controllers/actions
Here's what I came up with eventually, as a filter I add to the global filter collection for an MVC application:
/// <summary>
/// This filter should be applied to an MVC application as a global filter in RegisterGlobalFilters, not applied to individual actions/controllers.
/// It will cause access to every action to be DENIED by default.
/// If an AllowAnonymousAttribute is applied, all authorization checking is skipped (this takes precedence over AuthorizeSafeAttribute).
/// If an AuthorizeSafeAttribute is applied, only the roles specified in AuthorizeSafeAttribute's Roles property will be allowed access.
/// </summary>
public sealed class AuthorizeSafeFilter : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) {
throw new Exception("This class is intended to be applied to an MVC application as a global filter in RegisterGlobalFilters, not applied to individual actions/controllers. Use the AuthorizeSafeAttribute with individual actions/controllers.");
}
// Disable caching for this request
filterContext.HttpContext.Response.Cache.SetNoServerCaching();
filterContext.HttpContext.Response.Cache.SetNoStore();
// If AllowAnonymousAttribute applied, skip authorization
if (
filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) ||
filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
) {
return;
}
// Backup original roles
string rolesBackup = this.Roles;
// Look for AuthorizeSafeAttribute roles
bool foundRoles = false;
string foundRolesString = null;
object[] actionCustomAttributes = filterContext.ActionDescriptor.GetCustomAttributes(false);
object[] controllerCustomAttributes = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(false);
if (actionCustomAttributes.Any(attr => attr is AuthorizeSafeAttribute)) {
AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(actionCustomAttributes.First(attr => attr is AuthorizeSafeAttribute));
foundRoles = true;
foundRolesString = foundAttr.Roles;
}
else if (controllerCustomAttributes.Any(attr => attr is AuthorizeSafeAttribute)) {
AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(controllerCustomAttributes.First(attr => attr is AuthorizeSafeAttribute));
foundRoles = true;
foundRolesString = foundAttr.Roles;
}
if (foundRoles && !string.IsNullOrWhiteSpace(foundRolesString)) {
// Found valid roles string; use it as our own Roles property and auth normally
this.Roles = foundRolesString;
base.OnAuthorization(filterContext);
}
else {
// Didn't find valid roles string; DENY all access by default
filterContext.Result = new HttpUnauthorizedResult();
}
// Restore original roles
this.Roles = rolesBackup;
}
}
I also define this attribute:
/// <summary>
/// Represents an attribute that is used to restrict access by callers to an action method, in conjunction
/// with a global AuthorizeSafeFilter, DENYING all access by default.
/// </summary>
public class AuthorizeSafeAttribute : Attribute {
public string Roles { get; set; }
}
I apply AllowAnonymousAttribute to my login actions/controllers and AuthorizeSafeAttribute to other ones, but if I forget to apply these, access is denied by default. I wish ASP.NET MVC were as secure as this by default. :-)
I'm trying to make a wizard in MVC3 using Entity Framework. It needs to keep the state of an object (an article in this case) across a couple of steps.
I have a static variable in my controller that instantiates a new Article. In the different Actions I use TryUpdateModel to map the form to the static variable. The problem is, it seems that TryUpdateModel() updates the database as well. I need TryUpdateModel to do the automatic mapping, and update the static _article variable, but I don't want it to persist to the database until the last step!
N.B: I know there are a lot of possible solutions for creating a wizard in MVC, but I'd like to know what to do to make this way work, so please no alternatives for an MVC wizard-pattern.
Thanks.
namespace website.Controllers
{
public class ArticlesController : BaseController
{
// private static variable to hold the chosen article in the wizard
private static articles _article = new articles();
/// <summary>
/// Index page shows a list of articles in a webgrid
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
List<articles> _articles = Data.getArticles();
return View(_articles);
}
/// <summary>
/// First page of the article wizard
/// </summary>
/// <returns></returns>
public ActionResult BasicDetails(string id, string nextButton)
{
// back or next doesn't matter - store form values
if (_article != null) TryUpdateModel(_article);
if (nextButton != null)
{
return RedirectToAction("ArticleGroup");
}
else
{
_article = Data.GetArticleById(id);
return View(_article);
}
}
/// <summary>
/// Second page of the article wizard
/// </summary>
/// <returns></returns>
public ActionResult ArticleGroup(string nextButton, string backButton)
{
TryUpdateModel(_article);
if (backButton != null)
return RedirectToAction("BasicDetails");
else if (nextButton != null)
{
return RedirectToAction("Price");
}
else
{
return View(_article);
}
}
/// <summary>
/// Third page of the article wizard
/// </summary>
/// <returns></returns>
public ActionResult Price(string nextButton, string backButton)
{
TryUpdateModel(_article);
if (backButton != null)
{
return RedirectToAction("ArticleGroup");
}
else if (nextButton != null)
return RedirectToAction("LinkedClubs");
else
{
return View(_article);
}
}
/// <summary>
/// Last page of the article wizard
/// </summary>
/// <returns></returns>
public ActionResult LinkedClubs(string backButton)
{
if (backButton != null)
return RedirectToAction("Price");
else
return View(_article);
}
}
}
Rather than using a static variable to hold your state information (that is a critical error btw) you should pass a state bag holding the information that you need in between pages.
Usually data entities (entities mapped to database) and viewmodel entities (entities with that user works) used separately. When user posted data after some step - you make TryUpdateModel() to session object (specific for user, not for all application as static variable).
At last step you call business logic method UpdateModel(viewmodel), that update all columns by id of viewmodel, using all filled properties.
I don't know what I am missing, and I don't know what else to read to get it right. I will try this gray question to see if I get closer to the solution. I am building a .NET MVC application.
This application is authenticating with OpenID using DotNetOpenAuth Library, all that is working ok. Once a user is authenticate I rebcord the openid token in the database and create call the forms authentication like below.
FormsAuthentication.SetAuthCookie(confirmedUser.OpenID, false);
After that this user pass all authorize attribute in my code. Like below:
[Authorize]
public ActionResult About()
{
return View();
}
I don't know where to set the roles for a specific user. I am not using the Membership services.
I need to get working the attributes like below:
[Authorize(Roles="Administrator")]
public ActionResult About()
{
return View();
}
First of all, good for you for not using a membership provider. That just doesn't work well with OpenID.
To make roles work without a membership provider, you need to implement your own class that derives from System.Web.Security.RoleProvider. It's completely departed from authentication, which makes it easy for you. You just need to store with each of your users in your database which roles they belong to, and then your RoleProvider interacts with that database.
Once you write your role provider class, wire it up with this in your web.config file. This snippet should appear within your system.web section.
<roleManager enabled="true" defaultProvider="Database">
<providers>
<add name="Database" type="MyRoleProvider" />
</providers>
</roleManager>
Here's one role provider I wrote for an OpenID web application. It's written using Linq to Entities, but you can get the idea and implement it to work against your database.
public class MyRoleProvider : RoleProvider {
public override string ApplicationName {
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames) {
var users = from token in Global.DataContext.AuthenticationToken
where usernames.Contains(token.ClaimedIdentifier)
select token.User;
var roles = from role in Global.DataContext.Role
where roleNames.Contains(role.Name, StringComparer.OrdinalIgnoreCase)
select role;
foreach (User user in users) {
foreach (Role role in roles) {
user.Roles.Add(role);
}
}
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) {
var users = from token in Global.DataContext.AuthenticationToken
where usernames.Contains(token.ClaimedIdentifier)
select token.User;
var roles = from role in Global.DataContext.Role
where roleNames.Contains(role.Name, StringComparer.OrdinalIgnoreCase)
select role;
foreach (User user in users) {
foreach (Role role in roles) {
user.Roles.Remove(role);
}
}
}
public override void CreateRole(string roleName) {
Global.DataContext.AddToRole(new Role { Name = roleName });
}
/// <summary>
/// Removes a role from the data source for the configured applicationName.
/// </summary>
/// <param name="roleName">The name of the role to delete.</param>
/// <param name="throwOnPopulatedRole">If true, throw an exception if <paramref name="roleName"/> has one or more members and do not delete <paramref name="roleName"/>.</param>
/// <returns>
/// true if the role was successfully deleted; otherwise, false.
/// </returns>
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) {
Role role = Global.DataContext.Role.SingleOrDefault(r => r.Name == roleName);
if (role == null) {
return false;
}
if (throwOnPopulatedRole && role.Users.Count > 0) {
throw new InvalidOperationException();
}
Global.DataContext.DeleteObject(roleName);
return true;
}
/// <summary>
/// Gets an array of user names in a role where the user name contains the specified user name to match.
/// </summary>
/// <param name="roleName">The role to search in.</param>
/// <param name="usernameToMatch">The user name to search for.</param>
/// <returns>
/// A string array containing the names of all the users where the user name matches <paramref name="usernameToMatch"/> and the user is a member of the specified role.
/// </returns>
public override string[] FindUsersInRole(string roleName, string usernameToMatch) {
return (from role in Global.DataContext.Role
where role.Name == roleName
from user in role.Users
from authTokens in user.AuthenticationTokens
where authTokens.ClaimedIdentifier == usernameToMatch
select authTokens.ClaimedIdentifier).ToArray();
}
public override string[] GetAllRoles() {
return Global.DataContext.Role.Select(role => role.Name).ToArray();
}
public override string[] GetRolesForUser(string username) {
return (from authToken in Global.DataContext.AuthenticationToken
where authToken.ClaimedIdentifier == username
from role in authToken.User.Roles
select role.Name).ToArray();
}
public override string[] GetUsersInRole(string roleName) {
return (from role in Global.DataContext.Role
where string.Equals(role.Name, roleName, StringComparison.OrdinalIgnoreCase)
from user in role.Users
from token in user.AuthenticationTokens
select token.ClaimedIdentifier).ToArray();
}
public override bool IsUserInRole(string username, string roleName) {
Role role = Global.DataContext.Role.SingleOrDefault(r => string.Equals(r.Name, roleName, StringComparison.OrdinalIgnoreCase));
if (role != null) {
return role.Users.Any(user => user.AuthenticationTokens.Any(token => token.ClaimedIdentifier == username));
}
return false;
}
public override bool RoleExists(string roleName) {
return Global.DataContext.Role.Any(role => string.Equals(role.Name, roleName, StringComparison.OrdinalIgnoreCase));
}
}
I'm still learning this stuff as well, but you probably need to create a custom authorization attribute. Check this out.
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)]