Great! You have registered the authentication middleware and prepared the database. In the next step, you are going to implement basic user authentication for the Tic-Tac-Toe application.
The following example demonstrates how to modify the user registration and add a simple login form with a user login and password textbox for authenticating users:
- Add a new model called LoginModel to the Models folder:
public class LoginModel { [Required] public string UserName { get; set; } [Required] public string Password { get; set; } public string ReturnUrl { get; set; } }
- Add a new folder called Account to the Views folder, and then add a new file called Login.cshtml within this new folder. It will contain the login view:
@model TicTacToe.Models.LoginModel <div class="container"> <div id="loginbox" style="margin-top:50px;"
class="mainbox
col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2"> <div class="panel panel-info"> <div class="panel-heading"> <div class="panel-title">Sign In</div> </div> <div style="padding-top:30px" class="panel-body"> <div style="display:none" id="login-alert"
class="alert alert-danger col-sm-12"></div> <form id="loginform" class="form-horizontal"
role="form" asp-action="Login" asp-
controller="Account"> <input type="hidden" asp-for="ReturnUrl" /> <div asp-validation-summary="ModelOnly"
class="text-danger"></div> <div style="margin-bottom: 25px" class="input-
group"> <span class="input-group-addon"><i
class="glyphicon
glyphicon-user"></i></span> <input type="text" class="form-control"
asp-for="UserName" value=""
placeholder="username
or email"> </div> <div style="margin-bottom: 25px" class="input-
group"> <span class="input-group-addon"><i
class="glyphicon
glyphicon-lock"></i></span> <input type="password" class="form-control"
asp-for="Password" placeholder="password"> </div> <div style="margin-top:10px" class="form-group"> <div class="col-sm-12 controls"> <button type="submit" id="btn-login" href="#"
class="btn btn-success">Login</button> </div> </div> <div class="form-group"> <div class="col-md-12 control"> <div style="border-top: 1px solid#888;
padding-top:15px; font-size:85%"> Don't have an account? <a asp-action="Index"
asp-controller="UserRegistration">Sign Up
Here
</a> </div> </div> </div> </form> </div> </div> </div> </div>
- Update UserService, add a SignInManager private field, and then update the constructor:
... private SignInManager<UserModel> _signInManager; public UserService(ApplicationUserManager userManager,
ILogger<UserService> logger, SignInManager<UserModel>
signInManager) { ... _signInManager = signInManager; ... } ...
- Add a new method called SignInUser to UserService:
public async Task<SignInResult> SignInUser( LoginModel loginModel, HttpContext httpContext)
{
_logger.LogTrace($"signin user {loginModel.UserName}");
var stopwatch = new Stopwatch(); stopwatch.Start();
try
{
var user = await _userManager.FindByNameAsync
(loginModel.UserName);
var isValid = await _signInManager.CheckPasswordSignInAsync
(user, loginModel.Password, true);
if (!isValid.Succeeded) return SignInResult.Failed;
if (!await _userManager.IsEmailConfirmedAsync(user))
return SignInResult.NotAllowed;
var identity = new ClaimsIdentity
(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name,
loginModel.UserName));
identity.AddClaim(new Claim(ClaimTypes.GivenName,
user.FirstName));
identity.AddClaim(new Claim(ClaimTypes.Surname,
user.LastName));
identity.AddClaim(new Claim("displayName", $"
{user.FirstName} {user.LastName}"));
if (!string.IsNullOrEmpty(user.PhoneNumber))
identity.AddClaim(new Claim(ClaimTypes.HomePhone,
user.PhoneNumber));
identity.AddClaim(new Claim("Score", user.Score.
ToString()));
await httpContext.SignInAsync(CookieAuthenticationDefaults.
AuthenticationScheme,
new ClaimsPrincipal(identity), new AuthenticationProperties {
IsPersistent = false });
return isValid;
}
catch (Exception ex)
{
_logger.LogError($"cannot sign in user{ loginModel.UserName} - {
ex} ");
throw ex;
}
finally
{
stopwatch.Stop();
_logger.LogTrace($"sign in user {loginModel.UserName} finished
in
{ stopwatch.Elapsed} ");
}
}
Add another method, SignOutUser, to UserService and update the user service interface:
public async Task SignOutUser(HttpContext httpContext) { await _signInManager.SignOutAsync(); await httpContext.SignOutAsync(new
AuthenticationProperties {
IsPersistent = false }); return; }
- Add a new controller called AccountController to the Controllers folder:
public class AccountController : Controller { private IUserService _userService; public AccountController(IUserService userService) { _userService = userService; } }
Let's perform the steps as follows:
-
- Implement a new Login method in AccountController as follows:
public async Task<IActionResult> Login(string returnUrl) { return await Task.Run(() => { var loginModel = new LoginModel { ReturnUrl =
returnUrl }; return View(loginModel); }); } ...
-
- Add another implementation of the Login method that takes in a login model as a parameter:
[HttpPost] public async Task<IActionResult> Login(LoginModel loginModel) { if (ModelState.IsValid) { var result = await _userService.SignInUser(loginModel,
HttpContext); if (result.Succeeded) { if (!string.IsNullOrEmpty(loginModel.ReturnUrl)) return Redirect(loginModel.ReturnUrl); else return RedirectToAction("Index", "Home"); } else ModelState.AddModelError("", result.IsLockedOut ?"User
is locked" : "User is not allowed"); } return View(); }
-
- Add a new Logout method as follows:
public IActionResult Logout() { _userService.SignOutUser(HttpContext).Wait(); HttpContext.Session.Clear(); return RedirectToAction("Index", "Home"); }
- Update the Views/Shared/_Menu.cshtml file, and replace the existing code block at the top of the method:
@using Microsoft.AspNetCore.Http; @{ var email = User?.Identity?.Name ??
Context.Session.GetString("email"); var displayName = User.Claims.FirstOrDefault(
x => x.Type == "displayName")?.Value ??
Context.Session.GetString("displayName"); }
- Update the Views/Shared/_Menu.cshtml file to display either a display name element for already authenticated users or a login element for an authenticated user; for that, replace the final <li> element:
<li> @if (!string.IsNullOrEmpty(email)) { Html.RenderPartial("_Account",
new TicTacToe.Models.AccountModel { Email = email,
DisplayName = displayName }); } else { <a asp-area="" asp-controller="Account"
asp-action="Login">Login</a> } </li>
- Update the Views/Shared/_Account.cshtml file, and replace the Log Off and View Details links:
<a class="btn btn-danger btn-block" asp-controller="Account"
asp-action="Logout" asp-area="">Log Off</a> <a class="btn btn-default btn-block" asp-action="Index"
asp-controller="Home" asp-area="Account">View Details</a>
- Go to the ViewsSharedComponentsGameSession folder, and update the default.cshtml file to improve the visual representation by having our table as follows:
...
<table> @for (int rows = 0; rows < 3; rows++) { <tr style="height:150px;"> @for (int columns = 0; columns < 3; columns++) { <td style="width:150px; border:1px solid #808080;text-
align:center; vertical-align:middle"
id="@($"c_{rows}_{columns}")"> @{ var position = Model.Turns?.FirstOrDefault(turn =>
turn.X == columns && turn.Y == rows); if (position != null) { if (position.User == Model.User1) <i class="glyphicon glyphicon-unchecked"></i> else <i class="glyphicon glyphicon-remove-circle"></i> } else { <a class="btn btn-default btn-SetPosition"style=
"width:150px; min-height:150px;"
data-X="@columns" data-Y="@rows"> </a> } } </td> } </tr> } </table> ...
- Start the application, click on the Login element in the top menu, and sign in as an existing user (or register as a user if you have not done so previously):
- Click the Log Off button. You should be logged off and get redirected back to the Home page:
That essentially makes up our forms authentication, where we have been able to sign a user in and out with a login form. In the next section, we will look at how we can add an external provider as a means of authentication to our application.