Search code examples
asp.net-mvcasp.net-mvc-4asp.net-membershipsimplemembership

Asp mvc 4 Membership and WebSecurity


I need a suggestion about what to do. I'm currently using WebSecurity methods to do all the account related job. However it does not support E-mail uniqueness verification so I have a few options:

  1. Write (subclass) a new SimpleMembershipProvider overwriting the existing createuserAndAccount method to verify the email address. But i would also have to implement the login-logout features (just as websecurity does) and a few others.

  2. Add uniqueness constraints on the database and catch them on my code. However this would cause me to be DataBase dependent.

  3. This might be a little bit cheap, but i could copy/paste the WebSecurity source code (since its open) on a new class, and modify the createUserAndAccount method.

Any other option? I'm aiming for the option 3 at the moment, would be the fastest way. On a side note, on the future I will be requiring roles as well and I'm not sure if WebSecurity provides support for them.


Solution

  • If it were me, I'd probably go about it the following way:

    First, assuming you're using SimpleMembership with Entity Framework or some database connection (ADO, LINQ to SQL, etc.) you're going to have two components: WebSecurity.* method calls, and the database connection to make profile changes. Personally, I'd add the CONSTRAINT to the database to ensure your data is pure, but you can also implement a membership service that handles this logic, too.

    First, group these in to an interface that can be referenced in your controller (something like the following):

    public interface IMembershipService
    {
        Int32 CurrentUserId { get; }
        String CurrentUserName { get; }
        Boolean IsAuthenticated { get; }
    
        Boolean CreateUserAndAccount(String username, String password, String emailaddress = null);
        Boolean CreateUserAndAccount(String username, string password, out String confirmationToken, String emailaddress = null);
        Boolean Login(String username, String password, Boolean persistCookie = false);
        void Logout();
    }
    

    Then you can implement the service as a hybrid of SimpleMembership and your database connection. For the sake of keeping it generic, I use the IRepository<T> pattern, but this could be a direct DbContext, ObjectContext, etc. I'm also keeping it brief, so excuse the missing checksums and short implementation.

    public class MembershipService : IMembershipService
    {
        protected readonly SimpleMembershipProvider membershiProvider;
        protected readonly SimpleRoleProvider roleProvider;
        protected readonly IRepository<UserProfile> profileRepository;
    
        public MembershipService(IRepository<UserProfile> profileRepository)
        {
            this.membershipProvider = Membership.Provider as SimpleMembershipProvider;
            this.roleProvider = Role.Provider as SimpleRoleProvider;
            this.profileRepository = userRepository;
        }
    
        #region IMembershipService Implementation
    
        public Int32 CurrentUserId
        {
            get { return WebSecurity.CurrentUserId; }
        }
        public String CurrentUserName
        {
            get { return WebSecurity.CurrentUserName; }
        }
        public Boolean IsAuthenticated
        {
            get { return WebSecurity.IsAuthenticated; }
        }
    
        public Boolean CreateUserAndAccount(String username, String password, String emailaddress = null)
        {
            // validate the email address is unique
            if (!this.profileRepository.Any(x => x.EmailAddress == emailaddress))
            {
                WebSecurity.CreateUserAndAccount(username, password, new
                {
                    EmailAddress = emailaddress
                }, createConfirmationToken);
                return true;
            }
            else
            {
                // handle the error how you see fit
                // (maybe even exception?)
                return false;
            }
        }
        public Boolean CreateUserAndAccount(String username, String password, out String confirmationToken, String emailaddress = null, out)
        {
            // validate the email address is unique
            if (this.profileRepository.First(x => x.EmailAddress == emailaddress) == null)
            {
                confirmationToken = WebSecurity.CreateUserAndAccount(username, password, new
                {
                    EmailAddress = emailaddress
                }, createConfirmationToken);
                return true;
            }
            else
            {
                // handle the error how you see fit
                // (maybe even exception?)
                confirmationToken = String.Empty;
                return false;
            }
        }
        public Boolean Login(String username, String password, Boolean persistCookie = false)
        {
            return WebSecurity.Login(username, password, persistCookie);
        }
        public void Logout()
        {
            WebSecurity.Logout();
        }
    
        #endregion
    }
    

    Now you can reference this interface in your controller and have the logic in one place. if you're using a DI container, obviously register it, but here's an example implementation:

    public class AccountController: Controller
    {
        private readonly IMembershipService membershipService;
    
        public AccountController(IMembershipService membershipService)
        {
            this.membershipService = membershipService;
        }
    
        /* ... */
    
        [HttpPost, ValidateAntiForgeryToken]
        public ActionResult Register(LoginViewModel model, String returnUrl)
        {
            if (ModelState.IsValid)
            {
                if (this.membershipService.CreateUserandAccount(model.Username, model.Password, model.EmailAddress))
                {
                    this.membershipService.Login(model.Username, model.Password);
                    if (!String.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    return RedirectToRoute("Default");
                }
                else
                {
                    ModelState.AddModelError("", "Unable to register.");
                }
            }
            return View(model);
        }
    
        /* ... */
    }
    

    If you're using EntityFramework, you can also use the IValidatableObject. To resist duplicating, here's another SO question/answer that checks for a unique entry:

    Entity Framework IValidatableObject