5.11. Authentication

The ASP.NET technology has a powerful mechanism for managing authentication in .NET applications called ASP.NET Identity. The infrastructure of OWIN and AspNet Identity make it possible to perform both standard authentication and authentication via external services through accounts in Google, Twitter, Facebook, et al.

The description of the ASP.NET Identity technology is quite comprehensive and goes beyond the scope of this publication but you can read about it at https://www.asp.net/identity.

For our application, we will take a less complicated approach based on form authentication. Enabling form authentication entails some changes in the web.config configuration file. Find the <system.web> section and insert the following subsection inside it:

  1. <authentication mode="Forms">
  2. <forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
  3. defaultUrl="~/Invoice/Index"/>
  4. </authentication>

Setting mode="Forms" enables form authentication. Some parameters need to follow it. The following list of parameters is available:

cookieless

specifies whether cookie sets are used and how they are used. It can take the following values:

  • UseCookies

    specifies that the cookie sets will always be used, regardless of the device

    UseUri

    cookies sets are never used

    AutoDetect

    if the device supports cookie sets, they are used, otherwise, they are not used; a test determining their support is run for this setting.

    UseDeviceProfile

    if the device supports cookie sets, they are used, otherwise, they are not used; no detection test is run. Used by default.

defaultUrl

specifies the URL to redirect to after authentication

domain

specifies cookie sets for the entire domain, allowing for the same cookie sets to be used for the main domain and its sub-domains. By default, its value is an empty string.

loginUrl

the URL for user authentication. The default value is "~/Account/Login".

name

specifies the name for the cookie set. The default value is ".ASPXAUTH".

path

specifies the path for the cookie set. The default value is "/".

requireSSL

specifies whether an SSL connection is required for sending cookie sets. The default value is false

timeout

specifies the timeout for cookies in minutes.

In our application, we will store authentication data in the same database that stores all other data to avoid the need for an additional connection string.

5.11.1. Infrastructure for Authentication

Now we need to create all the infrastructure required for authentication — models, controllers and views. The WebUser model describes the user:

  1. [Table("Firebird.WEBUSER")]
  2. public partial class WEBUSER
  3. {
  4. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
  5. "CA2214:DoNotCallOverridableMethodsInConstructors")]
  6. public WEBUSER()
  7. {
  8. WEBUSERINROLES = new HashSet<WEBUSERINROLE>();
  9. }
  10. [Key]
  11. [DatabaseGenerated(DatabaseGeneratedOption.None)]
  12. public int WEBUSER_ID { get; set; }
  13. [Required]
  14. [StringLength(63)]
  15. public string EMAIL { get; set; }
  16. [Required]
  17. [StringLength(63)]
  18. public string PASSWD { get; set; }
  19. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
  20. "CA2227:CollectionPropertiesShouldBeReadOnly")]
  21. public virtual ICollection<WEBUSERINROLE> WEBUSERINROLES { get; set; }
  22. }

We’ll add two more models: one for the description of roles (WEBROLE) and another one for binding the roles to users (WEBUSERINROLE).

  1. [Table("Firebird.WEBROLE")]
  2. public partial class WEBROLE
  3. {
  4. [Key]
  5. [DatabaseGenerated(DatabaseGeneratedOption.None)]
  6. public int WEBROLE_ID { get; set; }
  7. [Required]
  8. [StringLength(63)]
  9. public string NAME { get; set; }
  10. }
  1. [Table("Firebird.WEBUSERINROLE")]
  2. public partial class WEBUSERINROLE
  3. {
  4. [Key]
  5. [DatabaseGenerated(DatabaseGeneratedOption.None)]
  6. public int ID { get; set; }
  7. [Required]
  8. public int WEBUSER_ID { get; set; }
  9. [Required]
  10. public int WEBROLE_ID { get; set; }
  11. public virtual WEBUSER WEBUSER { get; set; }
  12. public virtual WEBROLE WEBROLE { get; set; }
  13. }

We will use the Fluent API to specify relations between WEBUSER and WEBUSERINROLE in the DbModel class.

  1. public virtual DbSet<WEBUSER> WEBUSERS { get; set; }
  2. public virtual DbSet<WEBROLE> WEBROLES { get; set; }
  3. public virtual DbSet<WEBUSERINROLE> WEBUSERINROLES { get; set; }
  4. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  5. {
  6. modelBuilder.Entity<WEBUSER>()
  7. .HasMany(e => e.WEBUSERINROLES)
  8. .WithRequired(e => e.WEBUSER)
  9. .WillCascadeOnDelete(false);
  10. }

Since we use the Database First technology, tables in the database can be created automatically. I prefer to control the process so here is a script for creating the additional tables:

  1. RECREATE TABLE WEBUSER (
  2. WEBUSER_ID INT NOT NULL,
  3. EMAIL VARCHAR(63) NOT NULL,
  4. PASSWD VARCHAR(63) NOT NULL,
  5. CONSTRAINT PK_WEBUSER PRIMARY KEY(WEBUSER_ID),
  6. CONSTRAINT UNQ_WEBUSER UNIQUE(EMAIL)
  7. );
  8. RECREATE TABLE WEBROLE (
  9. WEBROLE_ID INT NOT NULL,
  10. NAME VARCHAR(63) NOT NULL,
  11. CONSTRAINT PK_WEBROLE PRIMARY KEY(WEBROLE_ID),
  12. CONSTRAINT UNQ_WEBROLE UNIQUE(NAME)
  13. );
  14. RECREATE TABLE WEBUSERINROLE (
  15. ID INT NOT NULL,
  16. WEBUSER_ID INT NOT NULL,
  17. WEBROLE_ID INT NOT NULL,
  18. CONSTRAINT PK_WEBUSERINROLE PRIMARY KEY(ID)
  19. );
  20. ALTER TABLE WEBUSERINROLE
  21. ADD CONSTRAINT FK_WEBUSERINROLE_USER
  22. FOREIGN KEY (WEBUSER_ID) REFERENCES WEBUSER (WEBUSER_ID);
  23. ALTER TABLE WEBUSERINROLE
  24. ADD CONSTRAINT FK_WEBUSERINROLE_ROLE
  25. FOREIGN KEY (WEBROLE_ID) REFERENCES WEBROLE (WEBROLE_ID);
  26. RECREATE SEQUENCE SEQ_WEBUSER;
  27. RECREATE SEQUENCE SEQ_WEBROLE;
  28. RECREATE SEQUENCE SEQ_WEBUSERINROLE;
  29. SET TERM ^;
  30. RECREATE TRIGGER TBI_WEBUSER
  31. FOR WEBUSER
  32. ACTIVE BEFORE INSERT
  33. AS
  34. BEGIN
  35. IF (NEW.WEBUSER_ID IS NULL) THEN
  36. NEW.WEBUSER_ID = NEXT VALUE FOR SEQ_WEBUSER;
  37. END^
  38. RECREATE TRIGGER TBI_WEBROLE
  39. FOR WEBROLE
  40. ACTIVE BEFORE INSERT
  41. AS
  42. BEGIN
  43. IF (NEW.WEBROLE_ID IS NULL) THEN
  44. NEW.WEBROLE_ID = NEXT VALUE FOR SEQ_WEBROLE;
  45. END^
  46. RECREATE TRIGGER TBI_WEBUSERINROLE
  47. FOR WEBUSERINROLE
  48. ACTIVE BEFORE INSERT
  49. AS
  50. BEGIN
  51. IF (NEW.ID IS NULL) THEN
  52. NEW.ID = NEXT VALUE FOR SEQ_WEBUSERINROLE;
  53. END^
  54. SET TERM ;^

To test it, we’ll add two users and two roles:

  1. INSERT INTO WEBUSER (EMAIL, PASSWD) VALUES ('john', '12345');
  2. INSERT INTO WEBUSER (EMAIL, PASSWD) VALUES ('alex', '123');
  3. COMMIT;
  4. INSERT INTO WEBROLE (NAME) VALUES ('admin');
  5. INSERT INTO WEBROLE (NAME) VALUES ('manager');
  6. COMMIT;
  7. -- Link users and roles
  8. INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(1, 1);
  9. INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(1, 2);
  10. INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(2, 2);
  11. COMMIT;
Comment about passwords

Usually, some hash from the password, rather than the actual password, is stored in an open form, using the PBKDF2 algorithm, for example. For our example, we have simplified authentication somewhat.

Our code will not interact directly with the WebUser model during registration and authentication. Instead, we will add some special models to the project:

  1. namespace FBMVCExample.Models
  2. {
  3. using System;
  4. using System.Collections.Generic;
  5. using System.ComponentModel.DataAnnotations;
  6. using System.ComponentModel.DataAnnotations.Schema;
  7. using System.Data.Entity.Spatial;
  8. // Login model
  9. public class LoginModel
  10. {
  11. [Required]
  12. public string Name { get; set; }
  13. [Required]
  14. [DataType(DataType.Password)]
  15. public string Password { get; set; }
  16. }
  17. // Model for registering a new user
  18. public class RegisterModel
  19. {
  20. [Required]
  21. public string Name { get; set; }
  22. [Required]
  23. [DataType(DataType.Password)]
  24. public string Password { get; set; }
  25. [Required]
  26. [DataType(DataType.Password)]
  27. [Compare("Password", ErrorMessage = " Passwords do not match ")]
  28. public string ConfirmPassword { get; set; }
  29. }
  30. }

These models will be used for the authentication and registration views, respectively. The authentication view is coded as follows:

  1. @model FBMVCExample.Models.LoginModel
  2. @{
  3. ViewBag.Title = "Login";
  4. }
  5. <h2>Login</h2>
  6. @using (Html.BeginForm())
  7. {
  8. @Html.AntiForgeryToken()
  9. <div class="form-horizontal">
  10. @Html.ValidationSummary(true)
  11. <div class="form-group">
  12. @Html.LabelFor(model => model.Name,
  13. new { @class = "control-label col-md-2" })
  14. <div class="col-md-10">
  15. @Html.EditorFor(model => model.Name)
  16. @Html.ValidationMessageFor(model => model.Name)
  17. </div>
  18. </div>
  19. <div class="form-group">
  20. @Html.LabelFor(model => model.Password,
  21. new { @class = "control-label col-md-2" })
  22. <div class="col-md-10">
  23. @Html.EditorFor(model => model.Password)
  24. @Html.ValidationMessageFor(model => model.Password)
  25. </div>
  26. </div>
  27. <div class="form-group">
  28. <div class="col-md-offset-2 col-md-10">
  29. <input type="submit" value="Logon" class="btn btn-default" />
  30. </div>
  31. </div>
  32. </div>
  33. }
  34. @section Scripts {
  35. @Scripts.Render("~/bundles/jqueryval")
  36. }
  37. The registration view, in turn, is coded as follows:
  38. @model FBMVCExample.Models.RegisterModel
  39. @{
  40. ViewBag.Title = "Registration";
  41. }
  42. <h2>???????????</h2>
  43. @using (Html.BeginForm())
  44. {
  45. @Html.AntiForgeryToken()
  46. <div class="form-horizontal">
  47. @Html.ValidationSummary(true)
  48. <div class="form-group">
  49. @Html.LabelFor(model => model.Name,
  50. new { @class = "control-label col-md-2" })
  51. <div class="col-md-10">
  52. @Html.EditorFor(model => model.Name)
  53. @Html.ValidationMessageFor(model => model.Name)
  54. </div>
  55. </div>
  56. <div class="form-group">
  57. @Html.LabelFor(model => model.Password,
  58. new { @class = "control-label col-md-2" })
  59. <div class="col-md-10">
  60. @Html.EditorFor(model => model.Password)
  61. @Html.ValidationMessageFor(model => model.Password)
  62. </div>
  63. </div>
  64. <div class="form-group">
  65. @Html.LabelFor(model => model.ConfirmPassword,
  66. new { @class = "control-label col-md-2" })
  67. <div class="col-md-10">
  68. @Html.EditorFor(model => model.ConfirmPassword)
  69. @Html.ValidationMessageFor(model => model.ConfirmPassword)
  70. </div>
  71. </div>
  72. <div class="form-group">
  73. <div class="col-md-offset-2 col-md-10">
  74. <input type="submit" value="Register"
  75. class="btn btn-default" />
  76. </div>
  77. </div>
  78. </div>
  79. }
  80. @section Scripts {
  81. @Scripts.Render("~/bundles/jqueryval")
  82. }
Comment about users

The model, views and controllers for user authentication and registration are made as simple as possible in this example. A user usually has a lot more attributes than just a username and a password.

Now let us add one more controller — AccountController — with the following contents:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Mvc;
  6. using System.Web.Security;
  7. using FBMVCExample.Models;
  8. namespace FBMVCExample.Controllers
  9. {
  10. public class AccountController : Controller
  11. {
  12. public ActionResult Login()
  13. {
  14. return View();
  15. }
  16. [HttpPost]
  17. [ValidateAntiForgeryToken]
  18. public ActionResult Login(LoginModel model)
  19. {
  20. if (ModelState.IsValid)
  21. {
  22. // search user in db
  23. WEBUSER user = null;
  24. using (DbModel db = new DbModel())
  25. {
  26. user = db.WEBUSERS.FirstOrDefault(
  27. u => u.EMAIL == model.Name &&
  28. u.PASSWD == model.Password);
  29. }
  30. // if you find a user with a login and password,
  31. // then remember it and do a redirect to the start page
  32. if (user != null)
  33. {
  34. FormsAuthentication.SetAuthCookie(model.Name, true);
  35. return RedirectToAction("Index", "Invoice");
  36. }
  37. else
  38. {
  39. ModelState.AddModelError("",
  40. " A user with such a username and password does not exist ");
  41. }
  42. }
  43. return View(model);
  44. }
  45. [Authorize(Roles = "admin")]
  46. public ActionResult Register()
  47. {
  48. return View();
  49. }
  50. [HttpPost]
  51. [ValidateAntiForgeryToken]
  52. public ActionResult Register(RegisterModel model)
  53. {
  54. if (ModelState.IsValid)
  55. {
  56. WEBUSER user = null;
  57. using (DbModel db = new DbModel())
  58. {
  59. user = db.WEBUSERS.FirstOrDefault(u => u.EMAIL == model.Name);
  60. }
  61. if (user == null)
  62. {
  63. // create a new user
  64. using (DbModel db = new DbModel())
  65. {
  66. // get a new identifier using a sequence
  67. int userId = db.NextValueFor("SEQ_WEBUSER");
  68. db.WEBUSERS.Add(new WEBUSER {
  69. WEBUSER_ID = userId,
  70. EMAIL = model.Name,
  71. PASSWD = model.Password
  72. });
  73. db.SaveChanges();
  74. user = db.WEBUSERS.Where(u => u.WEBUSER_ID == userId)
  75. .FirstOrDefault();
  76. // find the role of manager
  77. // This role will be the default role, i.e.
  78. // will be issued automatically upon registration
  79. var defaultRole =
  80. db.WEBROLES
  81. .Where(r => r.NAME == "manager")
  82. .FirstOrDefault();
  83. // Assign the default role to the newly added user
  84. if (user != null && defaultRole != null)
  85. {
  86. db.WEBUSERINROLES.Add(new WEBUSERINROLE
  87. {
  88. WEBUSER_ID = user.WEBUSER_ID,
  89. WEBROLE_ID = defaultRole.WEBROLE_ID
  90. });
  91. db.SaveChanges();
  92. }
  93. }
  94. // if the user is successfully added to the database
  95. if (user != null)
  96. {
  97. FormsAuthentication.SetAuthCookie(model.Name, true);
  98. return RedirectToAction("Login", "Account");
  99. }
  100. }
  101. else
  102. {
  103. ModelState.AddModelError("",
  104. "User with such login already exists");
  105. }
  106. }
  107. return View(model);
  108. }
  109. public ActionResult Logoff()
  110. {
  111. FormsAuthentication.SignOut();
  112. return RedirectToAction("Login", "Account");
  113. }
  114. }
  115. }

Note the attribute [Authorize(Roles = "admin")] to stipulate that only a user with the admin role can perform the user registration operation. This mechanism is called an authentication filter. We will get back to it a bit later.

Adding a New User

We add a new user to the database during registration and check during authentication as to whether that user exists. If the user is found, we use form authentication to set a cookie, as follows:

  1. FormsAuthentication.SetAuthCookie(model.Name, true);

All information about a user in Asp.Net MVC is stored in the proprty HttpContext.User that implements the IPrincipal interface defined in the System.Security.Principal namespace.

The IPrincipal interface defines the Identity property that stores the object of the IIdentity interface describing the current user.

The IIdentity interface has the following properties:

AuthenticationType

authentication type

IsAuthenticated

returns true if the user is logged in

Name

the username in the system

To determine whether a user is logged in, ASP.NET MVC receives cookies from the browser and if the user is logged in, the property IIdentity.IsAuthenticated is set to true and the Name property gets the username as its value.

Next, we will add authentication items using the universal providers mechanism.

Universal Providers

Universal providers offer a ready-made authentication functionality. At the same time, these providers are flexible enough that we can redefine them to work in whatever way we need them to. It is not necessary to redefine and use all four providers. That is handy if we do not need all of the fancy ASP.NET Identity features, but just a very simple authentication system.

So, our next step is to redefine the role provider. To do this, we need to add the Microsoft.AspNet.Providers package using NuGet.

Defining the Role Provider

To define the role provider, first we add the Providers folder to the project and then add a new MyRoleProvider class to it:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Security;
  6. using FBMVCExample.Models;
  7. namespace FBMVCExample.Providers
  8. {
  9. public class MyRoleProvider : RoleProvider
  10. {
  11. /// <summary>
  12. /// Returns the list of user roles
  13. /// </summary>
  14. /// <param name="username">Username</param>
  15. /// <returns></returns>
  16. public override string[] GetRolesForUser(string username)
  17. {
  18. string[] roles = new string[] { };
  19. using (DbModel db = new DbModel())
  20. {
  21. // Get the user
  22. WEBUSER user = db.WEBUSERS.FirstOrDefault(
  23. u => u.EMAIL == username);
  24. if (user != null)
  25. {
  26. // fill in an array of available roles
  27. int i = 0;
  28. roles = new string[user.WEBUSERINROLES.Count];
  29. foreach (var rolesInUser in user.WEBUSERINROLES)
  30. {
  31. roles[i] = rolesInUser.WEBROLE.NAME;
  32. i++;
  33. }
  34. }
  35. }
  36. return roles;
  37. }
  38. /// <summary>
  39. /// Creating a new role
  40. /// </summary>
  41. /// <param name="roleName">Role name</param>
  42. public override void CreateRole(string roleName)
  43. {
  44. using (DbModel db = new DbModel())
  45. {
  46. WEBROLE newRole = new WEBROLE() { NAME = roleName };
  47. db.WEBROLES.Add(newRole);
  48. db.SaveChanges();
  49. }
  50. }
  51. /// <summary>
  52. /// Returns whether the user role is present
  53. /// </summary>
  54. /// <param name="username">User name</param>
  55. /// <param name="roleName">Role name</param>
  56. /// <returns></returns>
  57. public override bool IsUserInRole(string username, string roleName)
  58. {
  59. bool outputResult = false;
  60. using (DbModel db = new DbModel())
  61. {
  62. var userInRole =
  63. from ur in db.WEBUSERINROLES
  64. where ur.WEBUSER.EMAIL == username &&
  65. ur.WEBROLE.NAME == roleName
  66. select new { id = ur.ID };
  67. outputResult = userInRole.Count() > 0;
  68. }
  69. return outputResult;
  70. }
  71. public override void AddUsersToRoles(string[] usernames,
  72. string[] roleNames)
  73. {
  74. throw new NotImplementedException();
  75. }
  76. public override string ApplicationName
  77. {
  78. get { throw new NotImplementedException(); }
  79. set { throw new NotImplementedException(); }
  80. }
  81. public override bool DeleteRole(string roleName,
  82. bool throwOnPopulatedRole)
  83. {
  84. throw new NotImplementedException();
  85. }
  86. public override string[] FindUsersInRole(string roleName,
  87. string usernameToMatch)
  88. {
  89. throw new NotImplementedException();
  90. }
  91. public override string[] GetAllRoles()
  92. {
  93. throw new NotImplementedException();
  94. }
  95. public override string[] GetUsersInRole(string roleName)
  96. {
  97. throw new NotImplementedException();
  98. }
  99. public override void RemoveUsersFromRoles(string[] usernames,
  100. string[] roleNames)
  101. {
  102. throw new NotImplementedException();
  103. }
  104. public override bool RoleExists(string roleName)
  105. {
  106. throw new NotImplementedException();
  107. }
  108. }
  109. }

For the purpose of illustration, three methods are redefined:

GetRolesForUser

for obtaining a set of roles for a specified user

CreateRole

for creating a role

IsUserInRole

determines whether the user has a specified role in the system

Configuring the Role Provider for Use

To use the role provider in the application, we need to add its definition to the configuration file. Open the web.config file and remove the definition of providers added automatically during the installation of the Microsoft.AspNet.Providers package.

Next, we insert our provider within the system.web section:

  1. <system.web>
  2. <authentication mode="Forms">
  3. <forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
  4. defaultUrl="~/Invoice/Index"/>
  5. </authentication>
  6. <roleManager enabled="true" defaultProvider="MyRoleProvider">
  7. <providers>
  8. <add name="MyRoleProvider"
  9. type="FBMVCExample.Providers.MyRoleProvider" />
  10. </providers>
  11. </roleManager>
  12. </system.web>