From 4085267499cae06a60a3ffaeb9e16909e8076137 Mon Sep 17 00:00:00 2001 From: Moonbase Date: Sat, 26 Jul 2025 14:35:49 -0700 Subject: [PATCH] Implement Email Verification In Auth Flow --- qtc-net-server/Controllers/AuthController.cs | 70 ++++++++++++++++++- qtc-net-server/Program.cs | 1 - .../Services/EmailService/EmailService.cs | 2 +- .../Services/TokenService/ITokenService.cs | 1 + .../Services/TokenService/TokenService.cs | 34 +++++++++ .../Services/UserService/UserService.cs | 4 +- 6 files changed, 107 insertions(+), 5 deletions(-) diff --git a/qtc-net-server/Controllers/AuthController.cs b/qtc-net-server/Controllers/AuthController.cs index 3587482..bdb69d0 100644 --- a/qtc-net-server/Controllers/AuthController.cs +++ b/qtc-net-server/Controllers/AuthController.cs @@ -3,8 +3,12 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Razor.TagHelpers; using qtc_api.Dtos.User; +using qtc_api.Services.EmailService; +using System.IdentityModel.Tokens.Jwt; using System.Runtime.CompilerServices; +using System.Security.Claims; using System.Text.Json; +using ZstdSharp.Unsafe; namespace qtc_api.Controllers { @@ -13,17 +17,21 @@ namespace qtc_api.Controllers public class AuthController : ControllerBase { private readonly IUserService _userService; + private readonly IEmailService _emailService; private readonly ITokenService _tokenService; private readonly IHubContext _chatGWContext; + private readonly IConfiguration _configuration; private readonly ServerConfig serverConfig; private readonly DataContext dataContext; - public AuthController(IUserService userService, ITokenService tokenService, IHubContext chatGWContext, DataContext dataContext) + public AuthController(IUserService userService, ITokenService tokenService, IHubContext chatGWContext, DataContext dataContext, IConfiguration configuration, IEmailService emailService) { _userService = userService; _tokenService = tokenService; _chatGWContext = chatGWContext; + _configuration = configuration; + _emailService = emailService; serverConfig = JsonSerializer.Deserialize(JsonDocument.Parse(System.IO.File.ReadAllText("./ServerConfig.json"))); this.dataContext = dataContext; @@ -36,8 +44,14 @@ namespace qtc_api.Controllers { var response = await _userService.AddUser(userDto); await _chatGWContext.Clients.All.SendAsync("RefreshUserLists"); - if(response.Success != false) + if(response.Success != false && response.Data != null) { + // send confirmation email (shouldn't do anything if email confirmation is disabled) + var confirmationToken = _tokenService.GenerateEmailConfirmationToken(response.Data); + var confirmationUrl = $"{Request.Scheme}://{Request.Host}/api/auth/verify-email?token={confirmationToken}"; + + await _emailService.SendConfirmationEmail(response.Data.Email, response.Data.Username, confirmationUrl); + return Ok(response); } else { @@ -70,6 +84,15 @@ namespace qtc_api.Controllers }); } + if(!dbUser.Data.IsEmailVerified && _configuration.GetValue("EmailConfig:EmailConfirmationRequired")) + { + return Ok(new ServiceResponse + { + Message = "You need to verify your email on this server. Check your inbox or spam. If you have not received an email, click 'Resend Verification Email'", + Success = false + }); + } + if (dbUser.Data.Id == serverConfig.AdminUserId && dbUser.Data.Role != "Admin") { dbUser.Data.Role = "Admin"; @@ -96,5 +119,48 @@ namespace qtc_api.Controllers var response = await _tokenService.ValidateRefreshToken(token); return Ok(response); } + + [HttpPost("verify-email")] + public async Task> VerifyEmail(string token) + { + try + { + var handler = new JwtSecurityTokenHandler(); + handler.InboundClaimTypeMap = new Dictionary(); + + 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 && jwt.ValidTo.ToUniversalTime() < now) + { + user.Data.IsEmailVerified = true; + await dataContext.SaveChangesAsync(); + + return Ok("Email Verified! You may now login on the client."); + } + } else + { + return Ok("The User This Confirmation Link Is Associated With No Longer Exists."); + } + } + } + + return Ok("Token Invalid. You may need to be sent another confirmation link."); + } catch (SecurityTokenMalformedException) + { + return Ok("Token Invalid. You may need to be sent another confirmation link."); + } + } } } diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index 9ff283a..ef1c2fd 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -24,7 +24,6 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddDbContext(); -builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSignalR(); builder.Services.AddStackExchangeRedisCache(options => diff --git a/qtc-net-server/Services/EmailService/EmailService.cs b/qtc-net-server/Services/EmailService/EmailService.cs index 2df077f..3072e04 100644 --- a/qtc-net-server/Services/EmailService/EmailService.cs +++ b/qtc-net-server/Services/EmailService/EmailService.cs @@ -37,7 +37,7 @@ namespace qtc_api.Services.EmailService string emailSubject = "QtC.NET Email Confirmation"; // build confirmation email body - StringBuilder emailBody = new StringBuilder(); + StringBuilder emailBody = new(); emailBody.AppendLine("Hello! This email was used to create an account on a QtC.NET Server."); emailBody.AppendLine(); emailBody.AppendLine($"You can confirm your email by clicking here - {confirmUrl}"); diff --git a/qtc-net-server/Services/TokenService/ITokenService.cs b/qtc-net-server/Services/TokenService/ITokenService.cs index 6d0b67a..bb17f87 100644 --- a/qtc-net-server/Services/TokenService/ITokenService.cs +++ b/qtc-net-server/Services/TokenService/ITokenService.cs @@ -3,6 +3,7 @@ public interface ITokenService { public Task> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false); + public ServiceResponse GenerateEmailConfirmationToken(User user); public Task> ValidateAccessToken(string accessToken); public Task> ValidateRefreshToken(string refreshToken); public ServiceResponse GetValidationParams(); diff --git a/qtc-net-server/Services/TokenService/TokenService.cs b/qtc-net-server/Services/TokenService/TokenService.cs index 563691e..89213c0 100644 --- a/qtc-net-server/Services/TokenService/TokenService.cs +++ b/qtc-net-server/Services/TokenService/TokenService.cs @@ -78,6 +78,40 @@ namespace qtc_api.Services.TokenService return serviceResponse; } + public ServiceResponse GenerateEmailConfirmationToken(User user) + { + var serviceResponse = new ServiceResponse(); + + // Generate JWT Access Token + + List claims = new List() + { + 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(24), + signingCredentials: creds + ); + + var jwt = new JwtSecurityTokenHandler().WriteToken(token); + + serviceResponse.Success = true; + serviceResponse.Data = jwt; + + return serviceResponse; + } + public async Task> ValidateRefreshToken(string refreshToken) { var serviceResponse = new ServiceResponse(); diff --git a/qtc-net-server/Services/UserService/UserService.cs b/qtc-net-server/Services/UserService/UserService.cs index 07b09a5..51f6892 100644 --- a/qtc-net-server/Services/UserService/UserService.cs +++ b/qtc-net-server/Services/UserService/UserService.cs @@ -1,4 +1,6 @@ -namespace qtc_api.Services.UserService +using qtc_api.Services.EmailService; + +namespace qtc_api.Services.UserService { public class UserService : IUserService {