Implementing authentication

Authentication allows applications to identify a specific user. It is not used to manage user access rights, which is the role of authorization, nor is it used to protect data, which is the role of data protection.

There are several methods for authenticating application users, such as the following:

  • Basic user form authentication, using a login form with login and password boxes
  • Single Sign-On (SSO) authentication, where the user only authenticates once for all their applications within the context of their company
  • Social network external provider authentication (such as Facebook and LinkedIn)
  • Certificate or Public Key Infrastructure (PKI) authentication

ASP.NET Core 3 supports all these methods, but in this chapter, we will concentrate on forms authentication with a user login and password, and external provider authentication via Facebook.

In the following examples, you will see how to use those methods to authenticate application users, along with a number of more advanced features, such as email confirmation and password reset mechanisms.

And last but not the least, you will see how to implement two-factor authentication using the built-in ASP.NET Core 3 authentication features for your most critical applications.

Let's prepare the implementation of the different authentication mechanisms for the Tic-Tac-Toe application:

  1. Update the lifetime of UserService, GameInvitationService, and GameSessionService in the Startup class:
        services.AddTransient<IUserService, UserService>(); 
        services.AddScoped<IGameInvitationService,
GameInvitationService>(); services.AddScoped<IGameSessionService, GameSessionService>
();
  1. Update the Configure method within the Startup class, and call the authentication middleware directly after the Static Files Middleware:
        app.UseStaticFiles(); 
        app.UseAuthentication(); 
  1. Update UserModel to use it with the built-in ASP.NET Core Identity authentication features, and remove the Id and Email properties, which are already provided by the IdentityUser class:
        public class UserModel : IdentityUser<Guid> 
        { 
          [Display(Name = "FirstName")] 
          [Required(ErrorMessage = "FirstNameRequired")] 
          public string FirstName { get; set; } 
          [Display(Name = "LastName")] 
          [Required(ErrorMessage = "LastNameRequired")] 
          public string LastName { get; set; }     
          [Display(Name = "Password")] 
          [Required(ErrorMessage = "PasswordRequired"), 
DataType(DataType.Password)] public string Password { get; set; } [NotMapped] public bool IsEmailConfirmed{ get {
return EmailConfirmed; } } public System.DateTime? EmailConfirmationDate { get; set;
} public int Score { get; set; } }
Note that in the real world, we would advise also removing the Password property. However, we will keep it in the example for clarity and learning purposes.
  1. Add a new folder called Managers, add a new manager in the folder called ApplicationUserManager, and then add the following constructor:
public class ApplicationUserManager : UserManager<UserModel> 
{ 
private IUserStore<UserModel> _store;
DbContextOptions<GameDbContext> _dbContextOptions;
public ApplicationUserManager(DbContextOptions<GameDbContext> dbContextOptions,
IUserStore<UserModel> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<UserModel> passwordHasher, IEnumerable<IUserValidator<UserModel>> userValidators,IEnumerable<IPasswordValidator<UserModel>> passwordValidators, ILookupNormalizer Normalizer,IdentityErrorDescriber errors, IServiceProvider services,
ILogger<UserManager<UserModel>> logger) :
base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
_store = store;
_dbContextOptions = dbContextOptions;
}
... }
  • Let's have a look at the steps to have a fully functioning ApplicationUserManager class:

    1. Add a FindByEmailAsync method as follows:
          public override async Task<UserModel> FindByEmailAsync
(string email) { using (var dbContext = new GameDbContext
(_dbContextOptions)) { return await dbContext.Set<UserModel>
().FirstOrDefaultAsync(
x => x.Email == email); } }
    1. Add a FindByIdAsync method as follows: 
public override async Task<UserModel> FindByIdAsync(string 
userId) { using (var dbContext = new GameDbContext
(_dbContextOptions)) { Guid id = Guid.Parse(userId); return await dbContext.Set<UserModel>
().FirstOrDefaultAsync(
x => x.Id == id); } }
    1. Add an UpdateAsync method as follows: 
public override async Task<IdentityResult> UpdateAsync
(UserModel user) { using (var dbContext = new GameDbContext(_dbContextOptions)) { var current = await dbContext.Set<UserModel>
().FirstOrDefaultAsync(x => x.Id == user.Id); current.AccessFailedCount = user.AccessFailedCount; current.ConcurrencyStamp = user.ConcurrencyStamp; current.Email = user.Email; current.EmailConfirmationDate = user.EmailConfirmationDate; current.EmailConfirmed = user.EmailConfirmed; current.FirstName = user.FirstName; current.LastName = user.LastName; current.LockoutEnabled = user.LockoutEnabled; current.NormalizedEmail = user.NormalizedEmail; current.NormalizedUserName = user.NormalizedUserName; current.PhoneNumber = user.PhoneNumber; current.PhoneNumberConfirmed = user.PhoneNumberConfirmed; current.Score = user.Score; current.SecurityStamp = user.SecurityStamp; current.TwoFactorEnabled = user.TwoFactorEnabled; current.UserName = user.UserName; await dbContext.SaveChangesAsync(); return IdentityResult.Success; } }
    1. Add a ConfirmEmailAsync method as follows:  
public override async Task<IdentityResult> ConfirmEmailAsync(UserModel user, string token) 
  { 
    var isValid = await base.VerifyUserTokenAsync(user,
Options.Tokens.EmailConfirmationTokenProvider,
ConfirmEmailToken
Purpose, token); if (isValid) { using (var dbContext = new GameDbContext
(_dbContextOptions)) { var current = await dbContext.UserModels.
FindAsync(user.Id); current.EmailConfirmationDate = DateTime.Now; current.EmailConfirmed = true; await dbContext.SaveChangesAsync(); return IdentityResult.Success; } } return IdentityResult.Failed(); } }
  1. Update the Startup class, and register the ApplicationUserManager class:
        services.AddTransient<ApplicationUserManager>(); 
  1. Update UserService to work with the ApplicationUserManager class, with the constructor as follows:
public class UserService : IUserService
{ 
  private ILogger<UserService> _logger; 
  private ApplicationUserManager _userManager; 
  public UserService(ApplicationUserManager userManager, 
ILogger<UserService> logger) { _userManager = userManager; _logger = logger; var emailTokenProvider = new EmailTokenProvider<UserModel>(); _userManager.RegisterTokenProvider("Default",
emailTokenProvider); } ... }
  • The following additions are done to make use of the ApplicationUserManager class, register the authentication middleware and then prepare the database:

    1. Add two new methods, the first one called GetEmailConfirmationCode, as follows:
          public async Task<string> GetEmailConfirmationCode
(UserModel user) { return await _userManager.
GenerateEmailConfirmationTokenAsync(user); }
  •  
    1. Secondly, add a ConfirmEmail method as follows: 
public async Task<bool> ConfirmEmail(string email, string code) 
{ 
  var start = DateTime.Now; 
  _logger.LogTrace($"Confirm email for user {email}"); 
 
  var stopwatch = new Stopwatch(); stopwatch.Start(); 
 
   try 
   { 
      var user = await _userManager.FindByEmailAsync(email); 
      if (user == null) return false;  
      var result = await _userManager.ConfirmEmailAsync(user, 
code); return result.Succeeded; } catch (Exception ex) { _logger.LogError($"Cannot confirm email for user
{email} - {ex}"); return false; } finally { stopwatch.Stop(); _logger.LogTrace($"Confirm email for user finished in
{stopwatch.Elapsed}"); } }
    1. Update the RegisterUser method as follows: 
public async Task<bool> RegisterUser(UserModel userModel) 
{ 
    var start = DateTime.Now; 
    _logger.LogTrace($"Start register user {userModel.Email} - 
{start}"); var stopwatch = new Stopwatch(); stopwatch.Start(); try { userModel.UserName = userModel.Email; var result = await _userManager.CreateAsync
(userModel,userModel.Password); return result == IdentityResult.Success; } catch (Exception ex) { _logger.LogError($"Cannot register user
{userModel.Email} -
{ex}"); return false; } finally { stopwatch.Stop(); _logger.LogTrace($"Start register user {userModel.Email}
finished at {DateTime.Now} - elapsed
{stopwatch.Elapsed.
TotalSeconds} second(s)"); } }
    1. Update the GetUserByEmail, IsUserExisting, GetTopUsers, and UpdateUser methods and then update the user service interface:
           public async Task<UserModel> GetUserByEmail(string
email) { return await _userManager.FindByEmailAsync(email); } public async Task<bool> IsUserExisting(string email) { return (await _userManager.FindByEmailAsync(email)) !=
null; } public async Task<IEnumerable<UserModel>> GetTopUsers(
int numberOfUsers) { return await _userManager.Users.OrderByDescending( x =>
x.Score).ToListAsync(); } public async Task UpdateUser(UserModel userModel) { await _userManager.UpdateAsync(userModel); }
Note that you should also update the UserServiceTest class to work with the new constructor. For that, you will also have to create a mock for the UserManager class and pass it to the constructor. For the moment, you can just disable the unit test by commenting it out and updating it later. But don't forget to do it!
  1. Update the EmailConfirmation method in UserRegistrationController, and use the GetEmailConfirmationCode method you have added previously to retrieve the email code:
        var urlAction = new UrlActionContext 
        { 
          Action = "ConfirmEmail", 
          Controller = "UserRegistration", 
          Values = new { email, code =
await _userService.GetEmailConfirmationCode(user) }, Protocol = Request.Scheme, Host = Request.Host.ToString() };
  1. Update the ConfirmEmail method in UserRegistrationController; it has to call the ConfirmEmail method in UserService to finish the email confirmation:
        [HttpGet] 
        public async Task<IActionResult> ConfirmEmail(string email, 
string code) { var confirmed = await _userService.ConfirmEmail(email,
code); if (!confirmed) return BadRequest(); return RedirectToAction("Index", "Home"); }
  1. Add a new class called RoleModel in the Models folder, and make it inherit from IdentityRole<long>, as it will be used by the built-in ASP.NET Core Identity Authentication features:
        public class RoleModel : IdentityRole<Guid> 
        { 
          public RoleModel() 
          { 
          } 
 
          public RoleModel(string roleName) : base(roleName) 
          { 
          } 
        } 
  1. Update GameDbContext, and add a new DbSet for role models:
        public DbSet<RoleModel> RoleModels { get; set; } 
  1. Register the authentication service and the identity service in the Startup class, and then use the new role model you added previously:
        services.AddIdentity<UserModel, RoleModel>(options => 
        { 
          options.Password.RequiredLength = 1; 
          options.Password.RequiredUniqueChars = 0; 
          options.Password.RequireNonAlphanumeric = false; 
          options.Password.RequireUppercase = false; 
          options.SignIn.RequireConfirmedEmail = false; 
        }).AddEntityFrameworkStores<GameDbContext>
().AddDefaultTokenProviders(); services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.
AuthenticationScheme; options.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme; }).AddCookie();
  1. Update the communication middleware, remove the _userService private member from the class, and update the constructor accordingly:
        public CommunicationMiddleware(RequestDelegate next) 
        { 
          _next = next; 
        } 
  1. Update the two ProcessEmailConfirmation methods in the communication middleware, as they must be asynchronous in order to work with ASP.NET Core Identity. Stop using the privately defined private readonly IUserService _userService; user service, in preference to a locally defined user service in each of the two methods as follows:
        private async Task ProcessEmailConfirmation(HttpContext 
context,
WebSocket currentSocket, CancellationToken ct, string
email) { var userService = context.RequestServices.
GetRequiredService<IUserService>(); ... } private async Task ProcessEmailConfirmation(HttpContext
context) { var userService = context.RequestServices.
GetRequiredService<IUserService>(); ... }
  1. Update GameInvitationService, and set the public constructor to static.
  2. Remove the following DbContextOptions registration from the Startup class; this will be replaced by another one in the next step:
        var dbContextOptionsbuilder = 
new DbContextOptionsBuilder<GameDbContext>() .UseSqlServer(connectionString); services.AddSingleton(dbContextOptionsbuilder.Options);
  1. Update the Startup class, and add a new DbContextOptions registration:
var connectionString = Configuration.
GetConnectionString("DefaultConnection");
services.AddScoped(typeof(DbContextOptions<GameDbContext>),
(serviceProvider) => { return new DbContextOptionsBuilder<GameDbContext>() .UseSqlServer(connectionString).Options; });
  1. Update the Configure method in the Startup class, and then replace the code that executes the database migration at the end of the method:
        var provider = app.ApplicationServices; 
        var scopeFactory = provider.
GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) using (var context = scope.ServiceProvider.
GetRequiredService<GameDbContext>()) { context.Database.Migrate(); }
  1. Update the Index method in GameInvitationController:
        ... 
        var invitation =
gameInvitationService.Add(gameInvitationModel).Result; return RedirectToAction("GameInvitationConfirmation",
new { id = invitation.Id }); ...
  1. Update the ConfirmGameInvitation method in GameInvitationController, and add additional fields to the existing user registration:
        await _userService.RegisterUser(new UserModel 
        { 
          Email = gameInvitation.EmailTo, 
          EmailConfirmationDate = DateTime.Now, 
          EmailConfirmed = true, 
          FirstName = "", 
          LastName = "", 
          Password = "Qwerty123!", 
          UserName = gameInvitation.EmailTo 
        }); 
Note that the automatic creation and registration of the invited user is only a temporary workaround that we have added to simplify the example application. In the real world, you will need to handle this case differently and replace the temporary workaround with a real solution.
  1. Update the CreateGameSession method in GameSessionService by passing in the invitedBy and invitedPlayer user models, instead of defining them internally as previously:
public async Task<GameSessionModel> CreateGameSession(
Guid invitationId, UserModel invitedBy, UserModel
invitedPlayer) { var session = new GameSessionModel { User1 = invitedBy, User2 = invitedPlayer, Id = invitationId, ActiveUser = invitedBy }; _sessions.Add(session); return session; }

Update the  AddTurn method in GameSessionService by passing in a user instead of getting the user by email as before, and then re-extract the GameSessionService interface:

    public async Task<GameSessionModel> AddTurn(Guid id, UserModel 
user, int x, int y) { ... turns.Add(new TurnModel
{
User = user,
X = x,
Y = y,
IconNumber = user.Email == gameSession.User1?
.Email ? "1" : "2"
});

gameSession.Turns = turns;
gameSession.TurnNumber = gameSession.TurnNumber + 1;

if (gameSession.User1?.Email == user.Email)
gameSession.ActiveUser = gameSession.User2; ... }
  1. Update the Index method in GameSessionController:
public async Task<IActionResult> Index(Guid id)
{
var session = await _gameSessionService.GetGameSession(id);
var userService = HttpContext.RequestServices.
GetService<IUserService>();
if (session == null)
{
var gameInvitationService = quest.HttpContext.RequestServices.
GetService<IGameInvitationService>();
var invitation = await gameInvitationService.Get(id);
var invitedPlayer = await userService.GetUserByEmail
(invitation.EmailTo);
var invitedBy = await userService.GetUserByEmail
(invitation.InvitedBy);
session = await _gameSessionService.CreateGameSession(
invitation.Id, invitedBy, invitedPlayer);
}
return View(session);
}
  1. Update the SetPosition method in GameSessionController, and pass turn.User instead of turn.User.Email (make sure that IGameSessionService has the following definition: Task<GameSessionModel> AddTurn(Guid id, UserModel user, int x, int y);):
        gameSession = await _gameSessionService.AddTurn(gameSession.Id,
turn.User, turn.X, turn.Y);
  1. Update the OnModelCreating method in GameDbContext, and add a WinnerId foreign key:
        ... 
        modelBuilder.Entity(typeof(GameSessionModel)) 
         .HasOne(typeof(UserModel), "Winner") 
         .WithMany() 
         .HasForeignKey("WinnerId")
.OnDelete(DeleteBehavior.Restrict); ...
  1. Update the GameInvitationConfirmation method in GameInvitationController to make it asynchronous. A controller action must be asynchronous in order to work with ASP.NET Core Identity:
        [HttpGet] 
        public async Task<IActionResult>
GameInvitationConfirmation(
Guid id, [FromServices]IGameInvitationService
gameInvitationService) { return await Task.Run(() => { var gameInvitation = gameInvitationService.Get(id).
Result; return View(gameInvitation); }); }
  1. Update the Index and SetCulture methods in HomeController so that they are asynchronous  in order to work with ASP.NET Core Identity:
        public async Task<IActionResult> Index() 
        { 
          return await Task.Run(() => 
          { 
            var culture = Request.HttpContext.Session.
GetString("culture"); ViewBag.Language = culture; return View(); }); } public async Task<IActionResult> SetCulture(string culture) { return await Task.Run(() => { Request.HttpContext.Session.SetString("culture",
culture); return RedirectToAction("Index"); }); }
  1. Update the Index method in UserRegistrationController and make it asynchronous to work with ASP.NET Core Identity:
        public async Task<IActionResult> Index() 
        { 
          return await Task.Run(() => 
          { 
            return View(); 
          }); 
        } 
  1. Open the Package Manager Console and execute the Add-Migration IdentityDb command.
  2. Update the database by executing the Update-Database command in the Package Manager Console.
  3. Start the application and register a new user, and then verify that everything is still working as expected.
Note that you have to use a complex password, such as Azerty123!, to be able to finish the user registration successfully now, since you have implemented the integrated features of ASP.NET Core Identity in this section, which requires complex passwords.

Well done for reaching this far, as our application is now ready to use ASP.NET Core Identity and, in general, it is now ready to handle different types of authentication, after all the preparation work in the preceding section. We are now at a good place to start learning how to add different types of authentication, and we start in the next section by looking at basic user form authentication. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.103.154