How do I create a custom membership provider for ASP.NET MVC 2?
Asked Answered
E

4

58

How do I create a custom membership for ASP.NET MVC 2 based on the ASP.NET membership provider?

Enochenol answered 5/5, 2010 at 6:43 Comment(0)
J
117

I have created a new project containing a custom membership provider and overrode the ValidateUser method from the MembershipProvider abstract class:

public class MyMembershipProvider : MembershipProvider
{ 
    public override bool ValidateUser(string username, string password)
    {    
        // this is where you should validate your user credentials against your database.
        // I've made an extra class so i can send more parameters 
        // (in this case it's the CurrentTerritoryID parameter which I used as 
        // one of the MyMembershipProvider class properties). 

        var oUserProvider = new MyUserProvider();  
        return oUserProvider.ValidateUser(username,password,CurrentTerritoryID);
    }
}

Then I connected that provider to my ASP.NET MVC 2 project by adding a reference and pointing it out from my web.config:

<membership defaultProvider="MyMembershipProvider">
    <providers>
        <clear />
        <add name="MyMembershipProvider"
            applicationName="MyApp"
            Description="My Membership Provider"
            passwordFormat="Clear"
            connectionStringName="MyMembershipConnection"
            type="MyApp.MyMembershipProvider" />
    </providers>
</membership>

I do need to create a custom class that inherits the RoleProvider abstract class and overrides the GetRolesForUser method. The ASP.NET MVC Authorizing uses that method to find out which roles are assigned to the current logged-on user and makes sure the user is permitted to access the controller action.

Here are the steps we need to take:

1) Create a custom class that inherits the RoleProvider abstract class and overrides the GetRolesForUser method:

public override string[] GetRolesForUser(string username)
{
    SpHelper db = new SpHelper();
    DataTable roleNames = null;
    try
    {
        // get roles for this user from DB...

        roleNames = db.ExecuteDataset(ConnectionManager.ConStr,
                    "sp_GetUserRoles",
                    new MySqlParameter("_userName", username)).Tables[0];
    }
    catch (Exception ex)
    {
        throw ex;
    }
    string[] roles = new string[roleNames.Rows.Count];
    int counter = 0;
    foreach (DataRow row in roleNames.Rows)
    {
        roles[counter] = row["Role_Name"].ToString();
        counter++;
    }
    return roles;
}

2) Connect the role provider with the ASP.NET MVC 2 application via our web.config:

<system.web>
...

<roleManager enabled="true" defaultProvider="MyRoleProvider">
    <providers>
        <clear />
        <add name="MyRoleProvider"
            applicationName="MyApp"
            type="MyApp.MyRoleProvider"
            connectionStringName="MyMembershipConnection" />
    </providers>
</roleManager>

...
</system.web>

3) Set the Authorize(Roles="xxx,yyy") above the wanted Controller / Action:

[Authorization(Roles = "Customer Manager,Content Editor")]
public class MyController : Controller
{
    ...... 
}

That's it! Now it works!

4) Optional: set a custom Authorize attribute so we can redirect an unwanted role to an AccessDenied Page:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class MyAuthorizationAttribute : AuthorizeAttribute
{
    /// <summary>
    /// The name of the master page or view to use when rendering the view on authorization failure.  Default
    /// is null, indicating to use the master page of the specified view.
    /// </summary>
    public virtual string MasterName { get; set; }

    /// <summary>
    /// The name of the view to render on authorization failure.  Default is "Error".
    /// </summary>
    public virtual string ViewName { get; set; }

    public MyAuthorizationAttribute ()
        : base()
    {
        this.ViewName = "Error";
    }

    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (AuthorizeCore(filterContext.HttpContext))
        {
            SetCachePolicy(filterContext);
        }
        else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            // auth failed, redirect to login page
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else if (filterContext.HttpContext.User.IsInRole("SuperUser"))
        {
            // is authenticated and is in the SuperUser role
            SetCachePolicy(filterContext);
        }
        else
        {
            ViewDataDictionary viewData = new ViewDataDictionary();
            viewData.Add("Message", "You do not have sufficient privileges for this operation.");
            filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
        }
    }

    protected void SetCachePolicy(AuthorizationContext filterContext)
    {
        // ** IMPORTANT **
        // Since we're performing authorization at the action level, the authorization code runs
        // after the output caching module. In the worst case this could allow an authorized user
        // to cause the page to be cached, then an unauthorized user would later be served the
        // cached page. We work around this by telling proxies not to cache the sensitive page,
        // then we hook our custom authorization code into the caching mechanism so that we have
        // the final say on whether a page should be served from the cache.
        HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
    }
}

Now we can use our own made attribute to redirect our users to access denied view:

[MyAuthorization(Roles = "Portal Manager,Content Editor", ViewName = "AccessDenied")]
public class DropboxController : Controller
{ 
    .......
}

That's it! Super duper!

Here are some of the links I've used to get all this info:

Custom role provider: http://davidhayden.com/blog/dave/archive/2007/10/17/CreateCustomRoleProviderASPNETRolePermissionsSecurity.aspx

I hope this info helps!

Javierjavler answered 6/5, 2010 at 10:41 Comment(15)
the way you've explained this is sensational!! and i bet you weren't even trying that hard... you should consider writing blog posts :).Approval
the way you've explained this is sensational!! and i bet you weren't even trying that hard... you should consider writing blog posts :).Approval
thanks buddy, glad it helped. i find myself do that often and by doing that i understand it better myself :-)Javierjavler
What does "new MyUserProvider();" and "CurrentTerritoryID" in fisrt code snippet mean? This looks pretty simple so I hope this will do the work :) Thanks!Enochenol
hi ile, basically ValidateUser method is where u should validate your user credentials against your database. i've made an extra class so i can send more parameters (in this case it's the CurrentTerritoryID parameter which i used as one of the MyMembershipProvider class). nevertheless, you can choose just to validate it there or go through another layer/class/methodJavierjavler
hmm... I don't understand this completely... is there any example code you maybe have? With this custom provider I would be able to use my own user and role tables? What about those existing tables in ASPNETDB? Sorry if I asked something non-sense, but I'm pretty new in ASP.NET... thanks!Enochenol
Btw, class MyMembershipProvider isn't complete? When I compile it I get a bunch of errors: MyMembershipProvider' does not implement inherited abstract member 'System.Web.Security.MembershipProvider.FindUsersByEmail(string, int, int, out int)' .... I suppose it must have all this methods: erictopia.com/2010/05/…Enochenol
that's because MembershipProvider is an abstract class with abstract methods that u must implement in your class. (those methods could be empty and do nothing if u'd like). about the ASPNETDB file, it's the default file for microsoft's membership schema. you could ask your membership provider to use the same schema at your DB Server instead of a file (using a connection string) or alternatively to use your own tables using custom membership provider like we did here...Javierjavler
I've managed to set it up. I used combination of your post and the one from uriDium's link: mattwrock.com/post/2009/10/14/…. Your post isn't complete and it confused me a bit. It is important that all methods are in the code, but those which are not used they must be defined as Not Implemented - that is the step that made me a lot of troubles. Thanks :)Enochenol
good point ile, i've used Visual Studio auto-completion for inheritance of abstract classes. what it does is automatically implement all abstract methods for you as Not Implemented and then u can choose which one you want to inject your custom code into...Javierjavler
How exactly did you use "Visual Studio auto-completion for inheritance of abstract classes"? Are you implying that Visual Studio recognized which methods are missing and it automatically "implemented" them as not implemented?Enochenol
no i mean that it just writes it for you. give it a shot: just start typing: "public class blablaMembership : MembershipProvider" and you'll see a small blue underscore under the M of the "MembershipProvider". click it and you'll have the option of "Implement Abstract Class 'MembershipProvider'" Once you click it, it'll write all the MembershipProvider methods with the implementation of: "throw new NotImplementedException();"Javierjavler
the way you've explained this is sensational!! and i bet you weren't even trying that hard... you should consider writing blog posts :).Oddment
Can you explain or show me about MyUserProvider() and CurrentTerritoryID? Thanks.Rostrum
See the explanation in the comments. it's just a class that validates your data against the DB and u can sent it whatever parameters you want. in this case i sent CurrentTerritoryID but u can build any class with any properties u needJavierjavler
R
10

This worked for me http://mattwrock.com/post/2009/10/14/Implementing-custom-Membership-Provider-and-Role-Provider-for-Authinticating-ASPNET-MVC-Applications.aspx

Refutation answered 5/5, 2010 at 6:47 Comment(2)
Can you summarize the main points from the link that answer the user's question as well as providing the link?Calvados
Web archive link for the same - hereNorthumbria
V
8

Its also possible to use this with a much smaller amount of code, i'm not entirely sure if this method is as safe but works very well with any database you use.

in the global.asax

protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.User != null)
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated)
            {
                if (HttpContext.Current.User.Identity is FormsIdentity)
                {
                    FormsIdentity id =
                        (FormsIdentity)HttpContext.Current.User.Identity;
                    FormsAuthenticationTicket ticket = id.Ticket;

                    // Get the stored user-data, in this case, our roles
                    string userData = ticket.UserData;
                    string[] roles = userData.Split(',');
                    HttpContext.Current.User = new GenericPrincipal(id, roles);
                }
            }
        }
    }

what this does is that it reads the roles from the authCookie which was made from FormsAuthenticationTicket

and the logon logic looks like this

public class dbService
{
    private databaseDataContext db = new databaseDataContext();

    public IQueryable<vwPostsInfo> AllPostsAndDetails()
    {
        return db.vwPostsInfos;
    }

    public IQueryable<role> GetUserRoles(int userID)
    {
        return (from r in db.roles
                    join ur in db.UsersRoles on r.rolesID equals ur.rolesID
                    where ur.userID == userID
                    select r);
    }

    public IEnumerable<user> GetUserId(string userName)
    {
        return db.users.Where(u => u.username.ToLower() == userName.ToLower());
    }

    public bool logOn(string username, string password)
    {
        try
        {
            var userID = GetUserId(username);
            var rolesIQueryable = GetUserRoles(Convert.ToInt32(userID.Select(x => x.userID).Single()));
            string roles = "";
            foreach (var role in rolesIQueryable)
            {
                roles += role.rolesName + ",";
            }

            roles.Substring(0, roles.Length - 2);
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                       1, // Ticket version
                       username, // Username associated with ticket
                       DateTime.Now, // Date/time issued
                       DateTime.Now.AddMinutes(30), // Date/time to expire
                       true, // "true" for a persistent user cookie
                       roles, // User-data, in this case the roles
                       FormsAuthentication.FormsCookiePath);// Path cookie valid for

            // Encrypt the cookie using the machine key for secure transport
            string hash = FormsAuthentication.Encrypt(ticket);
            HttpCookie cookie = new HttpCookie(
               FormsAuthentication.FormsCookieName, // Name of auth cookie
               hash); // Hashed ticket

            // Set the cookie's expiration time to the tickets expiration time
            if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;

            // Add the cookie to the list for outgoing response
            HttpContext.Current.Response.Cookies.Add(cookie);

            return true;
        }
        catch
        {
            return (false);
        }
    }
}

i store the roles in my database with two tables: table: Role which has the columns: roleID and roleName and the table: UsersRoles wich has the columns: userID and roleID, this makes it possible for multiple roles for several users and it's easy to make your own logic to add/remove roles from users and so forth. This enables you to use [Authorize(Roles="Super Admin")] for instance. hope this helps.

edit: forgot to make the password check but you just add an if in the logOn method which checks if the username and password provided checks up and if not it returns false

Vedetta answered 27/5, 2010 at 22:10 Comment(2)
Wait, so you're storing the role names in the auth cookie? Doesn't this mean the user can put whatever roles they want in their auth cookie? I guess it doesn't matter because they'd have to decrypt the cookie?Gasser
@Pandincus: Yes, that is one of the downsides of using this method if the user manages to decrypt the cookie, what one could do is to further encrypt the roles and provide an public key together with the cookie for later decryption in the global.asax. It's not perfect but it gets the job done and isn't that complex.Vedetta
M
1

I used the NauckIt.PostgreSQL provider's source code as a base, and modified it to suit my needs.

Mcmillin answered 5/5, 2010 at 22:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.