Integrating ASP.NET Membership’s Authentication and Authorization with Existing Corporate Database with MVC5, Entity Framework 6 – Part 3

woman working on code on a laptop

As discussed in Part 1 and Part 2 of this blog series, ASP.NET Membership allows you to validate and store user credentials with built-in functionalities. It helps you manage user authentication in the web application. ASP.NET Membership is a self-standing feature for authentication. It can also be integrated with ASP.NET role management to provide authorization services for the web applications. Membership can also be integrated with user profile to provide application-specific customization tailored to individual users.

In this Part 3 of the series, we will address questions 4 and 5 of the process.

  1. If you have an existing database, how do you enable it with authentication and authorization? Should you re-invent the wheel, or just use the ASP.NET membership provider?
  2. If we’re using ASP.NET membership, can we integrate it with existing database?
  3. How do you create an MVC application, if one does not already exist?
  4. How does one handle authentication and authorization?
  5. What’s the limitation of using the MVC5’s Role base data annotation? How do we overcome it?

STEP 4: Adding authentication and authorization

1. Modify Web.config. Add the following membership provider and role provider for ASP.NET Membership to point to the correct SQL database:

<!-- Configure the Sql Membership Provider -->
    <membership defaultProvider="SqlMembershipProvider" userIsOnlineTimeWindow="15">
      <providers>
        <clear />
        <add
          name="SqlMembershipProvider"
          type="System.Web.Security.SqlMembershipProvider"
          connectionStringName="ASPNETMembership"
          applicationName="/"
          enablePasswordRetrieval="false"
          enablePasswordReset="false"
          requiresQuestionAndAnswer="false"
          requiresUniqueEmail="true"
          passwordFormat="Hashed" />
      </providers>
    </membership>
    <!-- Configure the Sql Role Provider -->
    <roleManager enabled="true" defaultProvider="SqlRoleProvider" >
      <providers>
        <clear />
        <add name="SqlRoleProvider"
             type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             connectionStringName="ASPNETMembership"
             applicationName="/" />
      </providers>
    </roleManager>

2. Change the connection string from:

<add name="SchoolEntities"connectionString="metadata=res://*/SMSDB.csdl|res://*/SMSDB.ssdl|res://*/SMSDB.msl;provider=System.Data.EntityClient;provider connection string=&quot;data source=Long-X1E2;initial catalog=School;persist security info=True;user id=sa;password=!1LOCDevTheBest1!88;MultipleActiveResultSets=True;App=EntityFramework&quot;"providerName="System.Data.EntityClient" />

To:

    <add name="SchoolEntities"connectionString="metadata=res://*/SMSDB.csdl|res://*/SMSDB.ssdl|res://*/SMSDB.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=Long-X1E2;initial catalog=School;persist security info=True;user id=sa;password=!1LOCDevTheBest1!88;MultipleActiveResultSets=True;&quot;" providerName="System.Data.EntityClient" />
    <add name="ASPNETMembership" connectionString="data source=Long-X1E2;initial catalog=School;persist security info=True;user id=sa;password=!1LOCDevTheBest1!88;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />

This will avoid the exception: “Keyword not supported: metadata” since the original one is a EF connection string that contains a SQL Server connection string in its provider connection string parameter. The WebSecurity.InitializeDatabaseConnection expects a valid database connection string.

3. Add the following codes to AccountModel.cs:

namespace SchoolManagementSystem.Models
{
public class AccountModel
{
[Required]
[DataType(DataType.Text)]
[Display(Name = "User Name")]
[StringLength(255, MinimumLength = 6, ErrorMessage = "Must have a minimum length of 6")]
[RegularExpression("^[_A-Za-z0-9 ]+$", ErrorMessage = "Please enter a valid user name which contains letters, numbers, and/or space")]
public new string UserName { get; set; }    
[Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 8)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    [RegularExpression(@"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=(.*\W)).{6,}$", ErrorMessage = "Password must be 8 characters long, must contain at least one lower case letter, one upper case letter, one digit and one special character such as @, #, $, &, %, or !")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm Password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

public class LoginModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}

public class RegisterModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    [RegularExpression("^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", ErrorMessage = "Please enter a valid email address")]
    public new string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 8)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
}

4. Add the following codes to AccountController.cs:

namespace SchoolManagementSystem.Controllers
{
[Authorize]
public class AccountController : Controller
{
private ISMSRepository _smsRepository;    
public AccountController(ISMSRepository smsRepository)
    {
        _smsRepository = smsRepository;
    }
    public AccountController()
    {
        this._smsRepository = new SMSRepository(new SchoolEntities());
    }

    public UserManager<ApplicationUser> UserManager { get; private set; }

    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    //
    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            if (Membership.ValidateUser(model.UserName, model.Password))
            {
                FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                TempData["UserName"] = model.UserName;
                TempData["RememberMe"] = model.RememberMe;
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError("", "Invalid username or password.");
            }
        }
        TempData["RememberMe"] = model.RememberMe;
        return View(model);
    }

    //
    // GET: /Account/Register
    [AllowAnonymous]
    public ActionResult Register()
    {
        return View();
    }

    //
    // POST: /Account/Register
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public ActionResult Register(RegisterModel model)
    {
        if (ModelState.IsValid)
        {
            MembershipCreateStatus createStatus;
            Membership.CreateUser(model.UserName, model.Password, model.Email,
                   "question", "answer", true, null, out createStatus);

            if (createStatus == MembershipCreateStatus.Success)
            {
                //Assign Student role for a registered user by default
                var allRoles = Roles.GetAllRoles();

                Roles.AddUserToRole(model.UserName, "Student");
                FormsAuthentication.SetAuthCookie(model.UserName, false);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError(createStatus.ToString(), "Failed to register.");
            }
        }
        return View(model);
    }

    //
    // POST: /Account/LogOff
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult LogOff()
    {
        FormsAuthentication.SignOut();
        return RedirectToAction("Login", "Account");
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && UserManager != null)
        {
            UserManager.Dispose();
            UserManager = null;
        }
        base.Dispose(disposing);
    }

}
}

5. Edit the Login.cshtml as shown below. 

@model SchoolManagementSystem.Models.LoginModel
 
@{
    ViewBag.Title = "Log in";
}
 
<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>Use a local account to log in.</h4>
                <hr />
                @Html.ValidationSummary(true)
                <div class="form-group">
                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserName)
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password)
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("Register", "Register") if you don't have a local account.
                </p>
            }
        </section>
    </div>
    <div class="col-md-4">
        <section id="socialLoginForm">
            
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

6. Apply the following codes to Register.cshtml:

@model SchoolManagementSystem.Models.RegisterModel
@{
    ViewBag.Title = "Register";
}
 
<h2>@ViewBag.Title.</h2>
 
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Create a new account.</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
}
 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

STEP 5: What’s the limitation of using the MVC5’s Role base data annotation? How do we overcome it?

1. Create Custom Authorization – We’ll implement the multiple roles verification at the OnAuthorization(). Right click SchoolManagementSystem project -> Add “New Folder” called “Attributes” -> Right click “Attributes” folder -> Add a new class called “CustomAuthorizeAttribute.cs”. Code:

namespace SchoolManagementSystem.Attributes
{
    public class CustomAuthorizeAttribute : AuthorizeAttribute
    {
        SchoolEntities dbContext = new SchoolEntities();
        private readonly string[] authorizedRoles;
        public CustomAuthorizeAttribute(params string[] roles)
        {
            this.authorizedRoles = roles;
        }
 
        public override void OnAuthorization(AuthorizationContext authorizedContext)
        {
            bool authorize = false;
            if (authorizedContext.HttpContext.User.Identity.IsAuthenticated)    //Make sure user is authenticated, otherwise redirect to login page
            {
                foreach (var role in authorizedRoles)
                {
                    var isUserInRole = false;
                    if (authorizedContext.HttpContext.User.IsInRole(role))
                        isUserInRole = true;
                    authorize = isUserInRole ? true : false;
                }
                if (!authorize)
                {
                    authorizedContext.Result = new HttpUnauthorizedResult();
                }
            }
            else
            {
                authorizedContext.Result = new RedirectResult(string.Format("~/Account/Login?ReturnUrl={0}", HttpUtility.UrlEncode(authorizedContext.HttpContext.Request.RawUrl)));
            }
        }
    }
}

2. Then apply the CustomAuthorizeAttribute to each action of the PersonController.cs. For example, those who belong to “Admin,” “Faculty,” “Staff,” or “Student” can visit the index page of “Person.” To create a new person, you have to be “Admin” or “Faculty.”

  [CustomAuthorize("Admin", "Faculty", "Staff", "Student")]
        public async Task<ActionResult> Index()
        {
            var people = db.People.Include(p => p.OfficeAssignment).Include(p => p.UserPersonPermission);
            return View(await people.ToListAsync());
        }
 
        [CustomAuthorize("Admin", "Faculty")]
        public ActionResult Create()
        {
            ViewBag.PersonID = new SelectList(db.OfficeAssignments, "InstructorID", "Location");
            ViewBag.PersonID = new SelectList(db.UserPersonPermissions, "Id", "CreatedBy");
            return View();
        }

3. Then, apply the CustomAuthorization annotation to PersonController.

namespace SchoolManagementSystem.Controllers
{
    public class PersonController : Controller
    {
        private SchoolEntities db = new SchoolEntities();
 
        [CustomAuthorize("Admin", "Faculty", "Staff", "Student")]
        public async Task<ActionResult> Index()
        {
            var people = db.People.Include(p => p.OfficeAssignment).Include(p => p.UserPersonPermission);
            return View(await people.ToListAsync());
        }
 
        [CustomAuthorize("Admin", "Faculty", "Staff", "Student")]
        public async Task<ActionResult> Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Person person = await db.People.FindAsync(id);
            if (person == null)
            {
                return HttpNotFound();
            }
            return View(person);
        }
 
        [CustomAuthorize("Admin", "Faculty")]
        public ActionResult Create()
        {
            ViewBag.PersonID = new SelectList(db.OfficeAssignments, "InstructorID", "Location");
            ViewBag.PersonID = new SelectList(db.UserPersonPermissions, "Id", "CreatedBy");
            return View();
        }
 
        // POST: /Person/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [CustomAuthorize("Admin", "Faculty")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Create([Bind(Include="PersonID,LastName,FirstName,HireDate,EnrollmentDate,Discriminator")] Person person)
        {
            if (ModelState.IsValid)
            {
                db.People.Add(person);
                await db.SaveChangesAsync();
                return RedirectToAction("Index");
            }
 
            ViewBag.PersonID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", person.PersonID);
            ViewBag.PersonID = new SelectList(db.UserPersonPermissions, "Id", "CreatedBy", person.PersonID);
            return View(person);
        }
 
        [CustomAuthorize("Admin", "Faculty")]
        public async Task<ActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Person person = await db.People.FindAsync(id);
            if (person == null)
            {
                return HttpNotFound();
            }
            ViewBag.PersonID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", person.PersonID);
            ViewBag.PersonID = new SelectList(db.UserPersonPermissions, "Id", "CreatedBy", person.PersonID);
            return View(person);
        }
 
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [CustomAuthorize("Admin", "Faculty")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Edit([Bind(Include="PersonID,LastName,FirstName,HireDate,EnrollmentDate,Discriminator")] Personperson)
        {
            if (ModelState.IsValid)
            {
                db.Entry(person).State = EntityState.Modified;
                await db.SaveChangesAsync();
                return RedirectToAction("Index");
            }
            ViewBag.PersonID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", person.PersonID);
            ViewBag.PersonID = new SelectList(db.UserPersonPermissions, "Id", "CreatedBy", person.PersonID);
            return View(person);
        }
 
        [CustomAuthorize("Admin", "Faculty")]
        public async Task<ActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Person person = await db.People.FindAsync(id);
            if (person == null)
            {
                return HttpNotFound();
            }
            return View(person);
        }
 
        [CustomAuthorize("Admin", "Faculty")]
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> DeleteConfirmed(int id)
        {
            Person person = await db.People.FindAsync(id);
            db.People.Remove(person);
            await db.SaveChangesAsync();
            return RedirectToAction("Index");
        }
 
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

4. Change the RouteConfig.cs

  public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Account", action = "Login", id = UrlParameter.Optional }
            );
        }
}

5. Now start the debug instance, you’ll ask for login credentials to access the system. Register a user account if you haven’t already had one. The newly created account will be assigned “Student” role, and you can see the entire list of the student info. But it’s only for list purpose. The current implementation will redirect you to the Login page if you click the Edit/Create New/Delete links. You can redirect current logged-in user to the last visited page by modifying the OnAuthorization().

6. Download the finalized source code.


In conclusion, as explained and illustrated in Part I, Part 2, and Part 3 of this blog series, ASP.NET Membership is a powerful option for role management that is available to developers. It allows the creation and management of users. It can be customized according to the requirements of the web project, and also be easily integrated with existing corporate databases and applications.

ASP.NET Membership has evolved over the years. As we see in many applications, there is no longer a requirement to register our login credentials to use the application. Instead, we can just sign on to the external social networking websites, such as Facebook, using OAuth 2.0, to login to the application. Therefore, ASP.NET Identity was introduced to include a redirection-based external login to social media sites, such as Facebook, Twitter, Instagram and others. This is a future topic of discussion.