Firebird Documentation IndexFirebird 3.0 Developer's GuideCreating Web Applications in Entity Framework with MVC → Authentication
Firebird Home Firebird Home Prev: Creating a UI for Secondary ModulesFirebird Documentation IndexUp: Creating Web Applications in Entity Framework with MVCNext: Authorizing Access to Controller Methods

Authentication

Table of Contents

Infrastructure for 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 http://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.

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 md5 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>
            

Prev: Creating a UI for Secondary ModulesFirebird Documentation IndexUp: Creating Web Applications in Entity Framework with MVCNext: Authorizing Access to Controller Methods
Firebird Documentation IndexFirebird 3.0 Developer's GuideCreating Web Applications in Entity Framework with MVC → Authentication