Introduction
In the previous session, we demonstrated how to implement the three types of authorization. Now, we will extend the demo application by adding password and email change functionality.
This will be the final part of the series, where we will illustrate how to leverage .NET Core Identity's APIs to: generate password change and email change tokens and securely update the user’s email and password using these tokens.
Contents of this guide:
- Initial setup
- Reseting the account password
- Reseting the account email
Initial setup
To implement these two features, we need a way to send an email containing the appropriate reset password or email change link. While this is beyond the scope of this tutorial, you can find a simple email sender service implementation in one of my other posts.
To creat such a link we need the following configuration:
Step 1 - add the client base url to appsetings.json
and/or docker compose
file
{
//other settings
"Client": {
"ClientUrl": "https://www.my-awesome-webapp.com/"
}
}
#api configuration
environment:
- Client__ClientUrl=https://www.my-awesome-webapp.com/
Step 2 - create the ClientSettings
record
namespace DemoBackend.Configuration
{
public record ClientSettings
{
public required string ClientUrl { get; init; }
}
}
Step 3 - register the settings in Project.cs
file
builder.Services.Configure<ClientSettings>(builder.Configuration.GetSection("Client"));
Step 4 - register the client configurations
Inject the ClientSettings configuration inside the UserManagementController
:
namespace DemoBackend.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UserManagementController : ControllerBase
{
private readonly UserManager<UserEntity> _userManager;
private readonly AuthenticationSettings _authSettings;
private readonly ClientSettings _clientSettings;
private readonly ITokensRepository _tokensRepository;
public UserManagementController(UserManager<UserEntity> userManager, IOptions<AuthenticationSettings> authSettings,
IOptions<ClientSettings> clientSettings, ITokensRepository tokensRepository)
{
_userManager = userManager;
_authSettings = authSettings.Value;
_clientSettings = clientSettings.Value;
_tokensRepository = tokensRepository;
}
//omitted for brevity
}
}
Step 4 - EncryptionHelper
If we want to securely send the link we will need to encrypt the generated tokes. The following helper contains the methods to encrypt
and decrypt
a string:
using System.Security.Cryptography;
using System.Text;
namespace DemoBackend.Helpers
{
public static class EncryptionHelper
{
private static readonly string Bas64EncryptionKey = "I3PKTwfP1UkX4BOOBnifO/Ye6YEa7E0tDkp4QSQpRD4="; // Use a secure key
private static readonly string Base64EncryptionIV = "VU/3R0D9fZtvKf9zr6mNZw=="; // Use a secure key
public static string Encrypt(string plainText)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
using Aes aes = Aes.Create();
aes.Key = Convert.FromBase64String(Bas64EncryptionKey);
aes.IV = Convert.FromBase64String(Base64EncryptionIV);
using MemoryStream ms = new();
using (CryptoStream cs = new(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(plainBytes, 0, plainBytes.Length);
cs.Close();
}
return Convert.ToBase64String(ms.ToArray());
}
public static string Decrypt(string encryptedText)
{
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
using Aes aes = Aes.Create();
aes.Key = Convert.FromBase64String(Bas64EncryptionKey);
aes.IV = Convert.FromBase64String(Base64EncryptionIV);
using MemoryStream ms = new();
using (CryptoStream cs = new(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(encryptedBytes, 0, encryptedBytes.Length);
cs.Close();
}
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}
Please note that the encryption keys should be stored in a secrets management system. However, for the sake of the demo we have placed them inside the helper.
Reseting the account password
For this workflow, we need two methods:
- an
unauthorized
method to generate and send an email with thereset password link
(which includes the encrypted reset password token) to the user - another
unauthorized
method that will be called by the client app to accept thereset token
and thenew password
set by the user
Step 1 - SendResetPasswordNotification
method
Note: Since this is an unauthorized email, we also need to embed the user ID so that we can validate the user in the ResetPassword
method. This can be achieved by concatenating the token and user ID using the |
character, and then encrypting the resulting string.
namespace DemoBackend.Requests
{
public record ResetPasswordNotificationRequest
{
public required string Email { get; set; }
}
}
[HttpPost("send-reset-password-notification")]
public async Task<IActionResult> SendResetPasswordNotification([FromBody] ResetPasswordNotificationRequest resetPasswordEmailRequest)
{
try
{
var user = await _userManager.FindByEmailAsync(resetPasswordEmailRequest.Email);
if (user?.Email is null)
{
return BadRequest("User with this email not found.");
}
var passwordResetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
if (string.IsNullOrWhiteSpace(passwordResetToken))
{
return StatusCode(500, $"Internal server error");
}
var combinedToken = $"{passwordResetToken}|{user.Id}";
var encryptedToken = EncryptionHelper.Encrypt(combinedToken);
//create the reset password url
var resetPasswordUrl = $"{_clientSettings.ClientUrl}reset-password?token={encryptedToken}";
//send email with the reset password link
//encrypted token should only be sent by email, this is just for testing purposes
return Ok(encryptedToken);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
Step 2 - ResetPassword
method
Note: We will decrypt the token, split the reset password token from the user ID, and then use both values in the ResetPassword
method below.
namespace DemoBackend.Requests
{
public record ResetPasswordRequest
{
public required string NewPassword { get; set; }
public required string Token { get; set; }
}
}
[HttpPost("reset-password")]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest resetPasswordRequest)
{
try
{
var decryptedToken = EncryptionHelper.Decrypt(resetPasswordRequest.Token);
var tokenParts = decryptedToken.Split('|');
if (tokenParts.Length != 2)
{
return BadRequest("Invalid token.");
}
var passwordResetToken = tokenParts[0];
var userId = tokenParts[1];
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return BadRequest("User with this email not found.");
}
var result = await _userManager.ResetPasswordAsync(user, passwordResetToken,
resetPasswordRequest.NewPassword);
return result.Succeeded ? Ok("Password reset successfully.") : BadRequest("Password reset failed.");
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
Step 3 - Testing
- Send the password email:
curl --location 'http://localhost:8080/api/usermanagement/send-reset-password-notification' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "testuser1@example.com"
}'
Example link received:
https://www.my-awesome-webapp.com/reset-password?token=rIiPoR5qRfrM0d4Q8TX6ysnMHTtvLk1eo1tjaMSUk3rXvZwI+vqCiG/kevg/SLHtoCRX+5cd0Y4v6vQARkb4Q5pUhwwUGf+Jn2SsaC4DttccPv9kKRThuwHxUkEQuiVAI/KlYDuxT8MhpAcY5mr10p0NTj4P6vXUoL1KxLNGU0t6gWDspLogdYoZ214oI5IQqhH2WrzLuHq52cUwyJY0KhGDknR3eFYfOB3gpJYXhIXLU0V8p+29+zw2aghjkMDBPiaJO1MUM4TyHTOwhKPOv5FnI4fW3d/ScXxq7tFdG4E4gWiRsGZ8Zr6HrOGKxz6/oyLGJsBEDbHrtfkuT24+rWmQkKrOYys2Y5CY3hiM9YdwqlTzSZtyvTBV3mK+khG9
- Send a change password request using the token received by email:
//reset-password
curl --location 'http://localhost:8080/api/usermanagement/reset-password' \
--header 'Content-Type: application/json' \
--data '{
"newPassword": "test",
"token": "rIiPoR5qRfrM0d4Q8TX6ysnMHTtvLk1eo1tjaMSUk3rXvZwI+vqCiG/kevg/SLHtoCRX+5cd0Y4v6vQARkb4Q5pUhwwUGf+Jn2SsaC4DttccPv9kKRThuwHxUkEQuiVAI/KlYDuxT8MhpAcY5mr10p0NTj4P6vXUoL1KxLNGU0t6gWDspLogdYoZ214oI5IQqhH2WrzLuHq52cUwyJY0KhGDknR3eFYfOB3gpJYXhIXLU0V8p+29+zw2aghjkMDBPiaJO1MUM4TyHTOwhKPOv5FnI4fW3d/ScXxq7tFdG4E4gWiRsGZ8Zr6HrOGKxz6/oyLGJsBEDbHrtfkuT24+rWmQkKrOYys2Y5CY3hiM9YdwqlTzSZtyvTBV3mK+khG9"
}'
Reseting the account email
For this workflow, we need two methods:
- an
authorized
method to generate and send an email with thereset email link
(which includes the encrypted email reset token) to the user - another
authorized
method that will be called by the client app to accept thereset token
and thenew email
set by the user
Step 1 - SendResetEmailNotification
method
namespace DemoBackend.Requests
{
public record ResetEmailNotificationRequest
{
public required string NewEmail { get; set; }
}
}
[Authorize]
[HttpPost("send-reset-email-notification")]
public async Task<IActionResult> SendResetEmailNotification([FromBody] ResetEmailNotificationRequest resetEmailNotificationRequest)
{
try
{
var id = HttpContext.User.FindFirstValue(ApplicationClaims.Id);
if (id is null)
{
return Unauthorized();
}
var user = await _userManager.FindByIdAsync(id);
if (user is null)
{
return Unauthorized();
}
//create the reset password url
var emailToken = await _userManager.GenerateChangeEmailTokenAsync(user, resetEmailNotificationRequest.NewEmail);
var encryptedToken = EncryptionHelper.Encrypt(emailToken);
var resetPasswordUrl = $"{_clientSettings.ClientUrl}reset-email?token={encryptedToken}";
//send email with the reset email link
//encrypted token should only be sent by email, this is just for testing purposes
return Ok(encryptedToken);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
Step 2 - ResetEmail
method
namespace DemoBackend.Requests
{
public record ResetEmailReqest
{
public required string NewEmail { get; set; }
public required string Token { get; set; }
}
}
[Authorize]
[HttpPost("reset-email")]
public async Task<IActionResult> ResetEmail([FromBody] ResetEmailReqest resetEmailReqest)
{
try
{
var id = HttpContext.User.FindFirstValue(ApplicationClaims.Id);
if (id is null)
{
return Unauthorized();
}
var user = await _userManager.FindByIdAsync(id);
if (user is null)
{
return Unauthorized();
}
var decryptedToken = EncryptionHelper.Decrypt(resetEmailReqest.Token);
var result = await _userManager.ChangeEmailAsync(user, resetEmailReqest.NewEmail, decryptedToken);
return result.Succeeded ? Ok("Email changed successfully.") : BadRequest("Email change failed.");
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
Step 3 - Testing
- Log in with test user. You will need the
access token
for calling the email reset methods - Send the email reset email:
curl --location 'http://localhost:8080/api/usermanagement/send-reset-email-notification' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmYzAwYzkxMy0zYTk3LTRlYmQtYjQ2ZC1hNTQ0MjIwZjkwMjUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlcjFAZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMTYiLCJ1c2VyUm9sZSI6IlVTRVIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVU0VSIiwianRpIjoiYzNhMDRjYzktZDEzMC00NmJmLWJjZWQtOWZjOTc1ZTc5OGQ0IiwiYWNjZXNzVG9QcmVtaXVtIjoiYWNjZXNzVG9QcmVtaXVtIiwibmJmIjoxNzM4NTY0NjEwLCJleHAiOjE3Mzg1NjgyMTAsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImF1ZCI6Im15LXdlYi1hcGktY2xpZW50In0._qhwK9u45BASc4OWjx3lg2NJNFJOKT9wU7DO0AXUJ7Q' \
--data-raw '{
"newEmail": "testuser2@example.com"
}'
Example link received:
https://www.my-awesome-webapp.com/reset-email?token=rIiPoR5qRfrM0d4Q8TX6yldzAGtx83D+3b1H7MQvcOtlnxFhHL6wYVg2kveG+UXZqQDnStJHxaYm4fx/JSnR5SK2DMimtcbZyeLlBCa7cTcQflNG5QmzJzTDQjwnaijF+K1vWuc2+oI5BJz3bnH0GGBa8Kfjxyc/WxzVGMP/3F18K3qE5pOV4kMtSUztr+rctPni6e8Edy9oZjZGzwTHkMiB0CRB/bWZdSl7Ex0X+kXutYh0ywVBgff6W/C7FrBWPjkH6PEna5k3HSuWOaqRaiOHxL09Y4P5cMJ6i8wp5OQfMzalt3Ib+65ID28BhVtDQERHL39pPCQVhI+zp9ULDWagHIKP+JaEb+qVphh5y4L5drOua3qHE73hORvjCd0e
- Send a change email request using the token received by email:
//reset-password
curl --location 'http://localhost:8080/api/usermanagement/reset-email' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJmYzAwYzkxMy0zYTk3LTRlYmQtYjQ2ZC1hNTQ0MjIwZjkwMjUiLCJ1c2VyRW1haWwiOiJ0ZXN0dXNlcjFAZXhhbXBsZS5jb20iLCJ1c2VyQWdlIjoiMTYiLCJ1c2VyUm9sZSI6IlVTRVIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJVU0VSIiwianRpIjoiYzNhMDRjYzktZDEzMC00NmJmLWJjZWQtOWZjOTc1ZTc5OGQ0IiwiYWNjZXNzVG9QcmVtaXVtIjoiYWNjZXNzVG9QcmVtaXVtIiwibmJmIjoxNzM4NTY0NjEwLCJleHAiOjE3Mzg1NjgyMTAsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImF1ZCI6Im15LXdlYi1hcGktY2xpZW50In0._qhwK9u45BASc4OWjx3lg2NJNFJOKT9wU7DO0AXUJ7Q' \
--data-raw '{
"newEmail": "testuser2@example.com",
"token": "rIiPoR5qRfrM0d4Q8TX6yldzAGtx83D+3b1H7MQvcOtlnxFhHL6wYVg2kveG+UXZqQDnStJHxaYm4fx/JSnR5SK2DMimtcbZyeLlBCa7cTcQflNG5QmzJzTDQjwnaijF+K1vWuc2+oI5BJz3bnH0GGBa8Kfjxyc/WxzVGMP/3F18K3qE5pOV4kMtSUztr+rctPni6e8Edy9oZjZGzwTHkMiB0CRB/bWZdSl7Ex0X+kXutYh0ywVBgff6W/C7FrBWPjkH6PEna5k3HSuWOaqRaiOHxL09Y4P5cMJ6i8wp5OQfMzalt3Ib+65ID28BhVtDQERHL39pPCQVhI+zp9ULDWagHIKP+JaEb+qVphh5y4L5drOua3qHE73hORvjCd0e"
}'
Conclusions
In this final session, we have demonstrated how to implement secure reset password and reset email functionalities.
This concludes the series. Throughout this series, we have implemented the basic user management configuration using PostgreSQL, and explored token-based authentication and the various types of authorization.
You can find the code used in this series here. Happy coding!
Author Of article : Marian Salvan Read full article