ASP.NET Identity Flows
random picture

ASP.NET Identity Flows

On a authentication system, the user need to be able to do some tasks, this tasks was mentioned in the previous article, that here we are going do describe as flows.

The user need to:

  • Register
  • Login
  • Logout
  • Change password / Forgot password
  • Change information

There are some other minor flows that supports theses and give more security to this process like:

  • Account Confirmation
  • Multi Factor Authentication

In last article, we setup the dotnet project to support Authentication using ASP.NET Identity Framework, but we still able the user to register and login.

Before jumping to the flows it's important to highlight that, you have the option to let everything been deal with the ASP.NET Identity Framework when using .AddDefaultUI() option, that will include all pages that is needed, on the downside it′s that for personalize you will need to scaffold all this pages. All details can be found in here.

Registering Users

For the user be able to Login he need to register first.

Let's assume something first, you have a login page and a register page, in a application with MVC pattern applied, so in action that will receive the register form we can write the code below.

registerViewModel.ReturnUrl ??= Url.Content("~/");
registerViewModel.ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
if (ModelState.IsValid)
{
	var user = new ApplicationUser
	{
	   Email = registerViewModel.Email
	};

	var result = await _userManager.CreateAsync(user, registerViewModel.Password);

   if (result.Succeeded)
   {
	_logger.LogInformation("User created a new account with password.");

	var userId = await _userManager.GetUserIdAsync(user);
	var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
	code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
	var callbackUrl = Url.Page(
	"/Account/ConfirmEmail",
	pageHandler: null,
	values: new
	{
	area = "Identity", userId = userId, code = code, returnUrl = registerViewModel.ReturnUrl
	},
	protocol: Request.Scheme);

	if (callbackUrl != null)
		await _emailSender.SendEmailAsync(registerViewModel.Email, "Confirm your email", $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

	if (_userManager.Options.SignIn.RequireConfirmedAccount)
	{
		return RedirectToPage("RegisterConfirmation",
						new { email = registerViewModel.Email, returnUrl = registerViewModel.ReturnUrl });
	}
	else
	{
	 await _signInManager.SignInAsync(user, isPersistent: false);
	 return LocalRedirect(registerViewModel.ReturnUrl);
	}
}

foreach (var error in result.Errors)
{
  ModelState.AddModelError(string.Empty, error.Description);
}
}
// If we got this far, something failed, redisplay form
return View();        

In this code we are considering that the user need to confirm his email address, this step is required as a security measure.

As a security point, the code generated by ASP.NET Identity Framework in most recent versions NET6+ the code is very secure, been generated and encrypted and valid temporary and it's based on rotating encryption key.

For that we will need as well an action that will receive the code, as show below

if (userId == null || code == null)
{
	return RedirectToAction("Index", "Home");
}

var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
	return NotFound($"Unable to load user with ID '{userId}'.");
}

code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
var result = await _userManager.ConfirmEmailAsync(user, code);
ViewBag.StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
return View();        

One detail is that how this code is temporary, maybe you want to create a action that will resend the verification email, but will not be cover in this series.

Login Users

To login it's a very simple process using Identity Framework, you can use the PasswordSignInAsync() method from SignInManager, the example code is shown below:

ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
	var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
	if (result.Succeeded)
	{
		_logger.LogInformation(1, "User logged in.");
		return RedirectToLocal(returnUrl);
	}
	if (result.IsLockedOut)
	{
		_logger.LogWarning(2, "User account locked out.");
		return View("Lockout");
	}
	else
	{
		ModelState.AddModelError(string.Empty, "Invalid login attempt.");
		return View(model);
	}
}

// If we got this far, something failed, redisplay form
return View(model);        

The issue is that how we change the mechanism of the Identity Framework from cookies to JWT, so we need to change to something more like:

var generatedToken = _tokenService.BuildToken(_config["jwt:key"], _config["jwt:ValidIssuer"], user);
if (!string.IsNullOrEmpty(generatedToken))
{
	var result = await _signInManager.CheckPasswordSignInAsync(user, loginViewModel.Password,lockoutOnFailure: false);

	if (string.IsNullOrEmpty(HttpContext.Response.Headers.Authorization))
	{
		HttpContext.Response.Headers.Add("Authorization", "Bearer " + generatedToken);
	}
	HttpContext.Session.SetString("Token", generatedToken);
	if (result.Succeeded)
	{
		_logger.LogInformation(1, "User logged in.");
		return RedirectToLocal(returnUrl);
	}
}        

Logout

Logout is also a very simple process, we can take advantage of the Identity Framework method SignOutAsync() from SignInManager. As before we need to make some changes also to work with JWT, final code is shown below.

HttpContext.Session.Clear();
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
	return LocalRedirect(returnUrl);
}
else
{
	// This needs to be a redirect so that the browser performs a new
	// request and the identity for the user gets updated.
	return RedirectToAction("Index", "Home");
}        

Change Password / Forgot Password

This is a very sensitive topic due the relation with security, you can see more security details in here.

To change password we need to:

  • User is authenticated with active session.
  • Current password verification. This is to ensure that it's the legitimate user who is changing the password. The abuse case is this: a legitimate user is using a public computer to log in. This user forgets to log out. Then another person is using this public computer. If we don't verify the current password, this other person may be able to change the password.

This is also one feature present into Identity Framework, the code for Change password feature is shown below:

if (!ModelState.IsValid)
{
	return View(model);
}
var user = await GetCurrentUserAsync();
if (user != null)
{
	var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
	if (result.Succeeded)
	{
		await _signInManager.SignInAsync(user, isPersistent: false);
		_logger.LogInformation(3, "User changed their password successfully.");
		return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess });
	}
	AddErrors(result);
	return View(model);
}
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error });        

For the forgot password feature it's a little more complicated, because in this case we don't have the old password or even have the user login, for this we need to send a verification code for the email of the user.

First step will be the request for a forgot password flow, this need to require the user write his email address to a token been sent.

if (ModelState.IsValid)
{
	var user = await _userManager.FindByEmailAsync(model.Email);
	if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
	{
		// Don't reveal that the user does not exist or is not confirmed
		return View("ForgotPasswordConfirmation");
	}
	// Send an email with this link
	var code = await _userManager.GeneratePasswordResetTokenAsync(user);
	var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
	await _emailSender.SendEmailAsync(model.Email, "Reset Password",
	   "Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");
	return View("ForgotPasswordConfirmation");
}
// If we got this far, something failed, redisplay form
return View(model);        

The next step is when user click into the link in the email with the code, the code when returning on click on the link is shown below:

if (!ModelState.IsValid)
{
	return View(model);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
	// Don't reveal that the user does not exist
	return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
}
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded)
{
	return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
}
AddErrors(result);
return View();        

Change Information

This flow is very simple, if the user is login, he can change any personal information (except email/username, and password) without needed too much complex confirmation, you can just ask for the password.

To maintain the article not too long this will not be covered, as Account Confirmation and Multi-factor Authentication also will not be covered.

If you want that this be covered let a comment.

The next article will be cover Authorization.

Check the last articles:

ASP.NET Authentication and Authorization

ASP.NET Identity Framework


Stefan Xhunga

CEO | Kriselaengineering | Sales Certified - Software as a Service Solutions

8 个月

Luigi C. Filho ? Thank you for this interesting post! ? My comment: ???????ASP.NET Identity Framework provides a comprehensive set of tools and methodologies to implement robust authentication and authorization mechanisms in web applications. ? By leveraging these built-in features, developers can ensure a secure and seamless user experience. - **User Registration and Email Confirmation:** Ensures that only legitimate users can create accounts. - **User Login and JWT Integration:** ?Provides flexible authentication methods suitable for modern web applications. - **Password Management:** Offers secure password change and recovery options. - **Multi-Factor Authentication (MFA):** Adds an additional layer of security to user accounts. ?Understanding and implementing these flows correctly is crucial for building secure, scalable, and user-friendly authentication systems. ? By focusing on these core aspects, developers can enhance the security and usability of their applications, ultimately leading to higher user satisfaction and trust.

Ben Smith

Full Stack Developer

11 个月

Thanks for the code snippets and the clear explanation Luigi C. Filho ?

Tiago Reis

Senior Software Developer | ColdFusion (CFML), Lucee, PHP, JavaScript, SQL, Technical SEO | Creator of Educagaming | Passionate about Performance & Educational Game Development

11 个月

Luigi C. Filho ? awesome, well done, thank you for sharing ??

要查看或添加评论,请登录

Luigi C. Filho的更多文章

社区洞察

其他会员也浏览了