Implement Password Reset
This commit is contained in:
parent
08fb330de3
commit
3a530b6639
@ -138,6 +138,70 @@ namespace qtc_api.Controllers
|
||||
return Ok(new ServiceResponse<bool> { Success = false });
|
||||
}
|
||||
|
||||
[HttpPost("request-password-reset")]
|
||||
public async Task<ActionResult<ServiceResponse<bool>>> SendPasswordResetEmail(string email)
|
||||
{
|
||||
var user = await _userService.GetUserByEmail(email);
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
var resetToken = _tokenService.GenerateEmailConfirmationToken(user.Data); // we can probably use the same JWT structure for password resets
|
||||
var resetUrl = $"{Request.Scheme}://{Request.Host}/api/auth/start-password-reset?token={resetToken.Data}";
|
||||
|
||||
await _emailService.SendPasswordResetEmail(user.Data.Email, user.Data.Username, resetUrl);
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||
}
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = false });
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<ActionResult<ServiceResponse<bool>>> ResetPassword(string confirmationToken, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler()
|
||||
{
|
||||
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var jwt = handler.ReadJwtToken(confirmationToken);
|
||||
|
||||
if (jwt != null)
|
||||
{
|
||||
var email = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||
var id = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||
|
||||
if (email != null && id != null)
|
||||
{
|
||||
// get the user from id claim
|
||||
var user = await _userService.GetUserById(id.Value);
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (user.Data.Email == email.Value && now < jwt.ValidTo.ToUniversalTime())
|
||||
{
|
||||
user.Data.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password);
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(new ServiceResponse<bool> { Success = false, Message = "The User This Reset Token Is Associated With No Longer Exists." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||
}
|
||||
catch (SecurityTokenMalformedException)
|
||||
{
|
||||
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("verify-email")]
|
||||
public async Task<ActionResult<string>> VerifyEmail(string token)
|
||||
{
|
||||
@ -176,10 +240,55 @@ namespace qtc_api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link.");
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link. You can do this by clicking 'Resend Verification Email' in the client.");
|
||||
} catch (SecurityTokenMalformedException)
|
||||
{
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link.");
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link. You can do this by clicking 'Resend Verification Email' in the client.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("start-password-reset")]
|
||||
public async Task<ActionResult<string>> StartPasswordReset(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler()
|
||||
{
|
||||
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
|
||||
if (jwt != null)
|
||||
{
|
||||
var email = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||
var id = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||
|
||||
if (email != null && id != null)
|
||||
{
|
||||
// get the user from id claim
|
||||
var user = await _userService.GetUserById(id.Value);
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (user.Data.Email == email.Value && now < jwt.ValidTo.ToUniversalTime())
|
||||
{
|
||||
var clientToken = _tokenService.GeneratePasswordResetConfirmationToken(user.Data);
|
||||
return Ok($"To Reset Your Password, Paste The Following Code Into Your Client And Enter A New Password\n{clientToken.Data}\n\nNOTE: This code is only valid for one hour.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok("The User This Reset Link Is Associated With No Longer Exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||
}
|
||||
catch (SecurityTokenMalformedException)
|
||||
{
|
||||
return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace qtc_api.Services.EmailService
|
||||
|
||||
// build confirmation email body
|
||||
StringBuilder emailBody = new();
|
||||
emailBody.AppendLine($"<h1>Hello {name},</h1>");
|
||||
emailBody.AppendLine($"<h2>Hello {name},</h2>");
|
||||
emailBody.AppendLine("<p>Your receiving this message because you made a QtC.NET Account on a server that requires email confirmation.<br>");
|
||||
emailBody.AppendLine(@$"You can confirm your account by clicking <a href=""{confirmUrl}"">here.</a><br>");
|
||||
emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.<br><br>");
|
||||
@ -71,5 +71,64 @@ namespace qtc_api.Services.EmailService
|
||||
// disconnect
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
public async Task SendPasswordResetEmail(string email, string name, string passwordResetUrl)
|
||||
{
|
||||
// get config entries
|
||||
bool confirmationRequired = _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired");
|
||||
string? host = _configuration.GetValue<string>("EmailConfig:SMTPServer");
|
||||
string? username = _configuration.GetValue<string>("EmailConfig:SMTPUsername");
|
||||
string? password = _configuration.GetValue<string>("EmailConfig:SMTPPassword");
|
||||
string? senderAddress = _configuration.GetValue<string>("EmailConfig:SMTPSenderAddress");
|
||||
|
||||
if (!confirmationRequired)
|
||||
{
|
||||
_logger.LogInformation("Email Confirmation Is Disabled. For Better Security Of Your Instance, Consider Setting Up An Email Sender Service.");
|
||||
return;
|
||||
}
|
||||
else if (host == null || username == null || password == null || senderAddress == null)
|
||||
{
|
||||
_logger.LogInformation("Email Confirmation Is Enabled But No SMTP Settings Were Set.");
|
||||
return;
|
||||
}
|
||||
|
||||
// set email subject
|
||||
string emailSubject = "QtC.NET Email Confirmation";
|
||||
|
||||
// build confirmation email body
|
||||
StringBuilder emailBody = new();
|
||||
emailBody.AppendLine($"<h2>Hello {name},</h2>");
|
||||
emailBody.AppendLine("<p>Your receiving this message because you requested a password reset on a QtC.NET Account.<br>");
|
||||
emailBody.AppendLine(@$"You can reset your password by clicking <a href=""{passwordResetUrl}"">here.</a><br>");
|
||||
emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.<br><br>");
|
||||
emailBody.AppendLine("If you did not request a password reset on any QtC.NET account on any server, you may simply ignore this email.</p>");
|
||||
|
||||
// create new client
|
||||
using var client = new SmtpClient()
|
||||
{
|
||||
RequireTLS = true
|
||||
};
|
||||
|
||||
// connect and authenticate
|
||||
await client.ConnectAsync(host, 587);
|
||||
await client.AuthenticateAsync(username, password);
|
||||
|
||||
// construct email
|
||||
using var message = new MimeMessage();
|
||||
message.To.Add(new MailboxAddress(name, email));
|
||||
message.From.Add(new MailboxAddress("QtC.NET Server", senderAddress));
|
||||
|
||||
message.Subject = emailSubject;
|
||||
message.Body = new TextPart(MimeKit.Text.TextFormat.Html)
|
||||
{
|
||||
Text = emailBody.ToString()
|
||||
};
|
||||
|
||||
// send email
|
||||
await client.SendAsync(message);
|
||||
|
||||
// disconnect
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,5 +3,6 @@
|
||||
public interface IEmailService
|
||||
{
|
||||
public Task SendConfirmationEmail(string email, string name, string confirmUrl);
|
||||
public Task SendPasswordResetEmail(string email, string name, string passwordResetUrl);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
{
|
||||
public Task<ServiceResponse<string>> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false);
|
||||
public ServiceResponse<string> GenerateEmailConfirmationToken(User user);
|
||||
public ServiceResponse<string> GeneratePasswordResetConfirmationToken(User user);
|
||||
public Task<ServiceResponse<bool>> ValidateAccessToken(string accessToken);
|
||||
public Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken);
|
||||
public ServiceResponse<TokenValidationParameters> GetValidationParams();
|
||||
|
@ -112,6 +112,40 @@ namespace qtc_api.Services.TokenService
|
||||
return serviceResponse;
|
||||
}
|
||||
|
||||
public ServiceResponse<string> GeneratePasswordResetConfirmationToken(User user)
|
||||
{
|
||||
var serviceResponse = new ServiceResponse<string>();
|
||||
|
||||
// Generate JWT Access Token
|
||||
|
||||
List<Claim> claims = new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
new Claim(ClaimTypes.Email, user.Email)
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||
var issuer = _configuration["Jwt:Issuer"];
|
||||
var audience = _configuration["Jwt:Audience"];
|
||||
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(1),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||
|
||||
serviceResponse.Success = true;
|
||||
serviceResponse.Data = jwt;
|
||||
|
||||
return serviceResponse;
|
||||
}
|
||||
|
||||
public async Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken)
|
||||
{
|
||||
var serviceResponse = new ServiceResponse<string>();
|
||||
|
Loading…
x
Reference in New Issue
Block a user