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:
<authentication mode="Forms">
<forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
defaultUrl="~/Invoice/Index"/>
</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:
[Table("Firebird.WEBUSER")]
public partial class WEBUSER
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2214:DoNotCallOverridableMethodsInConstructors")]
public WEBUSER()
{
WEBUSERINROLES = new HashSet<WEBUSERINROLE>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int WEBUSER_ID { get; set; }
[Required]
[StringLength(63)]
public string EMAIL { get; set; }
[Required]
[StringLength(63)]
public string PASSWD { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<WEBUSERINROLE> WEBUSERINROLES { get; set; }
}
We’ll add two more models: one for the description of roles (WEBROLE) and another one for binding the roles to users (WEBUSERINROLE).
[Table("Firebird.WEBROLE")]
public partial class WEBROLE
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int WEBROLE_ID { get; set; }
[Required]
[StringLength(63)]
public string NAME { get; set; }
}
[Table("Firebird.WEBUSERINROLE")]
public partial class WEBUSERINROLE
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ID { get; set; }
[Required]
public int WEBUSER_ID { get; set; }
[Required]
public int WEBROLE_ID { get; set; }
public virtual WEBUSER WEBUSER { get; set; }
public virtual WEBROLE WEBROLE { get; set; }
}
We will use the Fluent API to specify relations between WEBUSER
and WEBUSERINROLE
in the DbModel
class.
…
public virtual DbSet<WEBUSER> WEBUSERS { get; set; }
public virtual DbSet<WEBROLE> WEBROLES { get; set; }
public virtual DbSet<WEBUSERINROLE> WEBUSERINROLES { get; set; }
…
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<WEBUSER>()
.HasMany(e => e.WEBUSERINROLES)
.WithRequired(e => e.WEBUSER)
.WillCascadeOnDelete(false);
…
}
…
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:
RECREATE TABLE WEBUSER (
WEBUSER_ID INT NOT NULL,
EMAIL VARCHAR(63) NOT NULL,
PASSWD VARCHAR(63) NOT NULL,
CONSTRAINT PK_WEBUSER PRIMARY KEY(WEBUSER_ID),
CONSTRAINT UNQ_WEBUSER UNIQUE(EMAIL)
);
RECREATE TABLE WEBROLE (
WEBROLE_ID INT NOT NULL,
NAME VARCHAR(63) NOT NULL,
CONSTRAINT PK_WEBROLE PRIMARY KEY(WEBROLE_ID),
CONSTRAINT UNQ_WEBROLE UNIQUE(NAME)
);
RECREATE TABLE WEBUSERINROLE (
ID INT NOT NULL,
WEBUSER_ID INT NOT NULL,
WEBROLE_ID INT NOT NULL,
CONSTRAINT PK_WEBUSERINROLE PRIMARY KEY(ID)
);
ALTER TABLE WEBUSERINROLE
ADD CONSTRAINT FK_WEBUSERINROLE_USER
FOREIGN KEY (WEBUSER_ID) REFERENCES WEBUSER (WEBUSER_ID);
ALTER TABLE WEBUSERINROLE
ADD CONSTRAINT FK_WEBUSERINROLE_ROLE
FOREIGN KEY (WEBROLE_ID) REFERENCES WEBROLE (WEBROLE_ID);
RECREATE SEQUENCE SEQ_WEBUSER;
RECREATE SEQUENCE SEQ_WEBROLE;
RECREATE SEQUENCE SEQ_WEBUSERINROLE;
SET TERM ^;
RECREATE TRIGGER TBI_WEBUSER
FOR WEBUSER
ACTIVE BEFORE INSERT
AS
BEGIN
IF (NEW.WEBUSER_ID IS NULL) THEN
NEW.WEBUSER_ID = NEXT VALUE FOR SEQ_WEBUSER;
END^
RECREATE TRIGGER TBI_WEBROLE
FOR WEBROLE
ACTIVE BEFORE INSERT
AS
BEGIN
IF (NEW.WEBROLE_ID IS NULL) THEN
NEW.WEBROLE_ID = NEXT VALUE FOR SEQ_WEBROLE;
END^
RECREATE TRIGGER TBI_WEBUSERINROLE
FOR WEBUSERINROLE
ACTIVE BEFORE INSERT
AS
BEGIN
IF (NEW.ID IS NULL) THEN
NEW.ID = NEXT VALUE FOR SEQ_WEBUSERINROLE;
END^
SET TERM ;^
To test it, we’ll add two users and two roles:
INSERT INTO WEBUSER (EMAIL, PASSWD) VALUES ('john', '12345');
INSERT INTO WEBUSER (EMAIL, PASSWD) VALUES ('alex', '123');
COMMIT;
INSERT INTO WEBROLE (NAME) VALUES ('admin');
INSERT INTO WEBROLE (NAME) VALUES ('manager');
COMMIT;
-- Link users and roles
INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(1, 1);
INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(1, 2);
INSERT INTO WEBUSERINROLE(WEBUSER_ID, WEBROLE_ID) VALUES(2, 2);
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:
namespace FBMVCExample.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
// Login model
public class LoginModel
{
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
// Model for registering a new user
public class RegisterModel
{
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = " Passwords do not match ")]
public string ConfirmPassword { get; set; }
}
}
These models will be used for the authentication and registration views, respectively. The authentication view is coded as follows:
@model FBMVCExample.Models.LoginModel
@{
ViewBag.Title = "Login";
}
<h2>Login</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Name,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Password,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password)
@Html.ValidationMessageFor(model => model.Password)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Logon" class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
The registration view, in turn, is coded as follows:
@model FBMVCExample.Models.RegisterModel
@{
ViewBag.Title = "Registration";
}
<h2>???????????</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Name,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Password,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password)
@Html.ValidationMessageFor(model => model.Password)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ConfirmPassword,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ConfirmPassword)
@Html.ValidationMessageFor(model => model.ConfirmPassword)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Register"
class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using FBMVCExample.Models;
namespace FBMVCExample.Controllers
{
public class AccountController : Controller
{
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
// search user in db
WEBUSER user = null;
using (DbModel db = new DbModel())
{
user = db.WEBUSERS.FirstOrDefault(
u => u.EMAIL == model.Name &&
u.PASSWD == model.Password);
}
// if you find a user with a login and password,
// then remember it and do a redirect to the start page
if (user != null)
{
FormsAuthentication.SetAuthCookie(model.Name, true);
return RedirectToAction("Index", "Invoice");
}
else
{
ModelState.AddModelError("",
" A user with such a username and password does not exist ");
}
}
return View(model);
}
[Authorize(Roles = "admin")]
public ActionResult Register()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
WEBUSER user = null;
using (DbModel db = new DbModel())
{
user = db.WEBUSERS.FirstOrDefault(u => u.EMAIL == model.Name);
}
if (user == null)
{
// create a new user
using (DbModel db = new DbModel())
{
// get a new identifier using a sequence
int userId = db.NextValueFor("SEQ_WEBUSER");
db.WEBUSERS.Add(new WEBUSER {
WEBUSER_ID = userId,
EMAIL = model.Name,
PASSWD = model.Password
});
db.SaveChanges();
user = db.WEBUSERS.Where(u => u.WEBUSER_ID == userId)
.FirstOrDefault();
// find the role of manager
// This role will be the default role, i.e.
// will be issued automatically upon registration
var defaultRole =
db.WEBROLES
.Where(r => r.NAME == "manager")
.FirstOrDefault();
// Assign the default role to the newly added user
if (user != null && defaultRole != null)
{
db.WEBUSERINROLES.Add(new WEBUSERINROLE
{
WEBUSER_ID = user.WEBUSER_ID,
WEBROLE_ID = defaultRole.WEBROLE_ID
});
db.SaveChanges();
}
}
// if the user is successfully added to the database
if (user != null)
{
FormsAuthentication.SetAuthCookie(model.Name, true);
return RedirectToAction("Login", "Account");
}
}
else
{
ModelState.AddModelError("",
"User with such login already exists");
}
}
return View(model);
}
public ActionResult Logoff()
{
FormsAuthentication.SignOut();
return RedirectToAction("Login", "Account");
}
}
}
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:
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using FBMVCExample.Models;
namespace FBMVCExample.Providers
{
public class MyRoleProvider : RoleProvider
{
/// <summary>
/// Returns the list of user roles
/// </summary>
/// <param name="username">Username</param>
/// <returns></returns>
public override string[] GetRolesForUser(string username)
{
string[] roles = new string[] { };
using (DbModel db = new DbModel())
{
// Get the user
WEBUSER user = db.WEBUSERS.FirstOrDefault(
u => u.EMAIL == username);
if (user != null)
{
// fill in an array of available roles
int i = 0;
roles = new string[user.WEBUSERINROLES.Count];
foreach (var rolesInUser in user.WEBUSERINROLES)
{
roles[i] = rolesInUser.WEBROLE.NAME;
i++;
}
}
}
return roles;
}
/// <summary>
/// Creating a new role
/// </summary>
/// <param name="roleName">Role name</param>
public override void CreateRole(string roleName)
{
using (DbModel db = new DbModel())
{
WEBROLE newRole = new WEBROLE() { NAME = roleName };
db.WEBROLES.Add(newRole);
db.SaveChanges();
}
}
/// <summary>
/// Returns whether the user role is present
/// </summary>
/// <param name="username">User name</param>
/// <param name="roleName">Role name</param>
/// <returns></returns>
public override bool IsUserInRole(string username, string roleName)
{
bool outputResult = false;
using (DbModel db = new DbModel())
{
var userInRole =
from ur in db.WEBUSERINROLES
where ur.WEBUSER.EMAIL == username &&
ur.WEBROLE.NAME == roleName
select new { id = ur.ID };
outputResult = userInRole.Count() > 0;
}
return outputResult;
}
public override void AddUsersToRoles(string[] usernames,
string[] roleNames)
{
throw new NotImplementedException();
}
public override string ApplicationName
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override bool DeleteRole(string roleName,
bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}
public override string[] FindUsersInRole(string roleName,
string usernameToMatch)
{
throw new NotImplementedException();
}
public override string[] GetAllRoles()
{
throw new NotImplementedException();
}
public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}
public override void RemoveUsersFromRoles(string[] usernames,
string[] roleNames)
{
throw new NotImplementedException();
}
public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}
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:
<system.web>
<authentication mode="Forms">
<forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
defaultUrl="~/Invoice/Index"/>
</authentication>
<roleManager enabled="true" defaultProvider="MyRoleProvider">
<providers>
<add name="MyRoleProvider"
type="FBMVCExample.Providers.MyRoleProvider" />
</providers>
</roleManager>
</system.web>