This commit is contained in:
Dhananjaya99
2026-07-08 11:11:50 +05:30
parent 2cd2741f77
commit de7b40a147
51 changed files with 6839 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
using AuthHex;
using AuthHex.Models;
using AuthHex.Utility;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace AuthSystem.API.Utils;
public class JwtTokenHelper
{
private readonly IConfiguration _config;
private readonly AppDBContext _dbContext;
private readonly RsaKeyProvider _rsaKeyProvider;
public JwtTokenHelper(IConfiguration config, AppDBContext dbContext, RsaKeyProvider rsaKeyProvider)
{
_config = config;
_dbContext = dbContext;
_rsaKeyProvider = rsaKeyProvider;
}
public async Task<string> GenerateToken(User user)
{
// Load user's role and usertype if not already loaded
if (user.Role == null)
{
await _dbContext.Entry(user)
.Reference(u => u.Role)
.LoadAsync();
}
if (user.UserType == null)
{
await _dbContext.Entry(user)
.Reference(u => u.UserType)
.LoadAsync();
}
var claims = new List<Claim>
{
new Claim("UserId", user.Id.ToString()),
new Claim("UserTypeId", user.UserTypeId.ToString()),
new Claim("UserTypeCode", user.UserType?.Code ?? ""),
new Claim("RoleId", user.RoleId.ToString()),
new Claim("RoleCode", user.Role?.Code ?? ""),
new Claim("NIC", user.Nic ?? ""),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,
new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
var privateKey = _rsaKeyProvider.GetPrivateKey();
var creds = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_config["Jwt:ExpiresInMinutes"]!)),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public Guid ExtractUserIdFromToken(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var publicKey = _rsaKeyProvider.GetPublicKey();
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = publicKey,
ValidateIssuer = true,
ValidIssuer = _config["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = _config["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userIdClaim = jwtToken.Claims.FirstOrDefault(x => x.Type == "UserId");
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out Guid userId))
{
return userId;
}
return Guid.Empty;
}
catch
{
return Guid.Empty;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using BCrypt.Net;
namespace AuthHex.Utility.passwordHasher;
public static class PasswordHasher
{
public static string Hash(string password) => BCrypt.Net.BCrypt.HashPassword(password);
public static bool Verify(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash);
}
+53
View File
@@ -0,0 +1,53 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace AuthHex.Utility;
public class RsaKeyProvider
{
private readonly RSA _rsa;
public RsaKeyProvider(string privateKeyXml)
{
_rsa = RSA.Create();
_rsa.FromXmlString(privateKeyXml);
}
public RsaSecurityKey GetPrivateKey()
{
var key = new RsaSecurityKey(_rsa);
var publicParameters = _rsa.ExportParameters(false);
key.KeyId = ComputeKeyId(publicParameters);
return key;
}
public RsaSecurityKey GetPublicKey()
{
var publicKeyRsa = RSA.Create();
var publicParameters = _rsa.ExportParameters(false);
publicKeyRsa.ImportParameters(publicParameters);
var key = new RsaSecurityKey(publicKeyRsa);
key.KeyId = ComputeKeyId(publicParameters);
return key;
}
private static string ComputeKeyId(RSAParameters publicParameters)
{
var modulus = publicParameters.Modulus ?? Array.Empty<byte>();
var exponent = publicParameters.Exponent ?? Array.Empty<byte>();
var payload = $"{Convert.ToBase64String(modulus)}.{Convert.ToBase64String(exponent)}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
return Base64UrlEncoder.Encode(hash);
}
public static (string privateKey, string publicKey) GenerateKeys()
{
using var rsa = RSA.Create(2048);
var privateKey = rsa.ToXmlString(true); // includes private key
var publicKey = rsa.ToXmlString(false); // public key only
return (privateKey, publicKey);
}
}
+567
View File
@@ -0,0 +1,567 @@
using AuthHex.Models.IOs;
using AuthHex.DTOs;
using System.Text.Json;
using System.Net.Mail;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace AuthHex.Utility
{
public class ThirdPartyService
{
private readonly IConfiguration _config;
private readonly HttpClient _httpClient;
public ThirdPartyService(IConfiguration config, HttpClient httpClient)
{
_config = config;
_httpClient = httpClient;
}
public async Task<ApiResponse> EmailConfiguration(string email, string otpCode, string? subject = null, string? body = null)
{
try
{
if (string.IsNullOrWhiteSpace(email))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Email is required"
};
}
if (string.IsNullOrWhiteSpace(otpCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "OTP code is required"
};
}
var testEmail = email;
var result = await SendEmailAsync(testEmail, otpCode, subject, body);
return new ApiResponse
{
StatusCode = result.Success ? 200 : 500,
Data = new
{
testEmail,
success = result.Success
},
Message = result.Message
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<OtpResponse> SendSmsAsync(string mobileNumber, string otpCode, string? message = null)
{
try
{
if (string.IsNullOrWhiteSpace(mobileNumber))
{
return new OtpResponse
{
StatusCode = 400,
Message = "Mobile number is required"
};
}
if (string.IsNullOrWhiteSpace(otpCode))
{
return new OtpResponse
{
StatusCode = 400,
Message = "OTP code is required"
};
}
string contactNumber = mobileNumber;
string apiUrl = _config["Sms:ApiUrl"] ?? "https://backup.introps.com/Sms_common/send_sms";
string apiKey = Environment.GetEnvironmentVariable("SMS_API_KEY") ?? _config["Sms:ApiKey"] ?? "8989898989";
var msgText = string.IsNullOrWhiteSpace(message) ? $"Your otp code is: {otpCode}" : message;
var smsRequest = new SmsRequest
{
api_key = apiKey,
phone_number = contactNumber,
msg_text = msgText
};
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("api_key", smsRequest.api_key),
new KeyValuePair<string, string>("phone_number", smsRequest.phone_number),
new KeyValuePair<string, string>("msg_text", smsRequest.msg_text)
});
var response = await _httpClient.PostAsync(apiUrl, formContent);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && responseContent.Contains("agent:d"))
{
string jsonString = responseContent.Split(new[] { "agent:d" }, StringSplitOptions.None)[1];
var responseJson = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonString);
if (responseJson is not null &&
responseJson.TryGetValue("stt", out var statusValue) &&
statusValue?.ToString() == "ok")
{
Console.WriteLine($"SMS OTP {otpCode} sent to {contactNumber}");
return new OtpResponse
{
StatusCode = 200,
Message = "OTP sent successfully."
};
}
}
Console.WriteLine($"Failed to send SMS OTP to {contactNumber}. Response: {responseContent}");
return new OtpResponse
{
StatusCode = 500,
Message = "Failed to send OTP.",
ApiResponse = responseContent
};
}
catch (Exception ex)
{
Console.WriteLine($"Exception sending OTP: {ex.Message}");
return new OtpResponse
{
StatusCode = 500,
Message = "Exception occurred while sending OTP.",
Error = ex.Message
};
}
}
private async Task<(bool Success, string Message)> SendEmailAsync(string email, string code, string? subject = null, string? body = null)
{
try
{
if (string.IsNullOrWhiteSpace(email))
{
return (false, "Email address is required");
}
string smtpHost = _config["SmtpSettings:SmtpServer"] ?? "smtp.hostinger.com";
int smtpPort = int.Parse(_config["SmtpSettings:SmtpPort"] ?? "587");
string? senderEmail = _config["SmtpSettings:SenderEmail"] ?? Environment.GetEnvironmentVariable("SMTP_USER");
string? senderPassword = _config["SmtpSettings:SmtpPassword"] ?? Environment.GetEnvironmentVariable("SMTP_PASSWORD");
string senderName = _config["SmtpSettings:SenderName"] ?? "Hexa Fitness";
if (string.IsNullOrWhiteSpace(senderEmail) || string.IsNullOrWhiteSpace(senderPassword))
{
return (false, "Email configuration is missing");
}
using (var smtpClient = new SmtpClient(smtpHost, smtpPort))
{
smtpClient.Credentials = new NetworkCredential(senderEmail, senderPassword);
smtpClient.EnableSsl = true;
smtpClient.Timeout = 10000;
var emailSubject = string.IsNullOrWhiteSpace(subject) ? "Your OTP Code" : subject;
var emailBody = string.IsNullOrWhiteSpace(body) ? $@"
<html>
<body style='font-family: Arial, sans-serif;'>
<h2>OTP Verification</h2>
<p>Your OTP code is:</p>
<h1 style='color: #007bff; letter-spacing: 2px;'>{code}</h1>
<p>This code will expire in 10 minutes.</p>
<p>If you did not request this code, please ignore this email.</p>
</body>
</html>" : body.Replace("{code}", code);
var mailMessage = new MailMessage
{
From = new MailAddress(senderEmail, senderName),
Subject = emailSubject,
Body = emailBody,
IsBodyHtml = true
};
mailMessage.To.Add(email);
await smtpClient.SendMailAsync(mailMessage);
Console.WriteLine($"Email OTP {code} sent to {email}");
return (true, "Email sent successfully");
}
}
catch (SmtpException ex)
{
Console.WriteLine($"SMTP Error sending email: {ex.Message}");
return (false, $"Failed to send email: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Exception sending email: {ex.Message}");
return (false, $"Exception occurred: {ex.Message}");
}
}
public string GenerateSecretKey(int? length = null)
{
var keyLength = length ?? int.Parse(_config["GoogleAuthenticator:SecretKeyLength"] ?? "32");
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var random = new Random();
var secret = new StringBuilder(keyLength);
for (int i = 0; i < keyLength; i++)
{
secret.Append(chars[random.Next(chars.Length)]);
}
return secret.ToString();
}
public string GenerateQrCodeData(string userEmail, string secretKey, string? issuer = null)
{
var appIssuer = issuer ?? _config["GoogleAuthenticator:Issuer"] ?? "AuthHex";
var encodedIssuer = Uri.EscapeDataString(appIssuer);
var encodedEmail = Uri.EscapeDataString(userEmail);
return $"otpauth://totp/{encodedIssuer}:{encodedEmail}?secret={secretKey}&issuer={encodedIssuer}";
}
public string GenerateTotpCode(string secretKey, DateTime? timeStamp = null)
{
var time = timeStamp ?? DateTime.UtcNow;
var unixTime = ((DateTimeOffset)time).ToUnixTimeSeconds();
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var timeStep = unixTime / timeWindow;
var secretBytes = Base32Decode(secretKey);
var timeBytes = BitConverter.GetBytes(timeStep);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(timeBytes);
}
using (var hmac = new HMACSHA1(secretBytes))
{
var hash = hmac.ComputeHash(timeBytes);
var offset = hash[hash.Length - 1] & 0x0F;
var code = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
return (code % 1000000).ToString("D6");
}
}
public bool ValidateTotpCode(string secretKey, string userCode, int? windowSize = null)
{
if (string.IsNullOrWhiteSpace(secretKey) || string.IsNullOrWhiteSpace(userCode))
{
return false;
}
var validationWindow = windowSize ?? int.Parse(_config["GoogleAuthenticator:ValidationWindowSize"] ?? "1");
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var currentTime = DateTime.UtcNow;
for (int i = -validationWindow; i <= validationWindow; i++)
{
var timeToCheck = currentTime.AddSeconds(i * timeWindow);
var generatedCode = GenerateTotpCode(secretKey, timeToCheck);
if (generatedCode == userCode)
{
return true;
}
}
return false;
}
public async Task<ApiResponse> InitiateTwoFASetup(string userEmail, string? userName = null)
{
try
{
if (string.IsNullOrWhiteSpace(userEmail))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Email is required"
};
}
var secretKey = GenerateSecretKey();
var issuer = _config["GoogleAuthenticator:Issuer"] ?? "AuthHex";
var qrCodeData = GenerateQrCodeData(userEmail, secretKey, issuer);
var displayName = string.IsNullOrWhiteSpace(userName) ? userEmail : userName;
return new ApiResponse
{
StatusCode = 200,
Data = new
{
secretKey,
qrCodeData,
manualEntryKey = FormatSecretForManualEntry(secretKey),
userEmail,
displayName,
issuer,
setupInstructions = new
{
step1 = "Install Google Authenticator app on your mobile device",
step2 = "Scan the QR code or enter the manual key",
step3 = "Enter the 6-digit code from your app to verify setup"
}
},
Message = "2FA setup initiated. Please verify with your authenticator app to complete setup."
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> CompleteTwoFASetup(string secretKey, string verificationCode, string? userEmail = null)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(verificationCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required"
};
}
// Validate the code to ensure user has successfully set up their authenticator
var isValid = ValidateTotpCode(secretKey, verificationCode);
if (!isValid)
{
return new ApiResponse
{
StatusCode = 400,
Message = "Invalid verification code. Please check your authenticator app and try again."
};
}
return new ApiResponse
{
StatusCode = 200,
Data = new
{
secretKey,
isVerified = true,
verifiedAt = DateTime.UtcNow,
userEmail,
backupCodes = GenerateBackupCodes() // Generate backup codes for recovery
},
Message = "2FA setup completed successfully. Please save your backup codes in a secure location."
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> VerifyTwoFACode(string secretKey, string code)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(code))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required"
};
}
var isValid = ValidateTotpCode(secretKey, code);
return new ApiResponse
{
StatusCode = isValid ? 200 : 400,
Data = new
{
isValid,
timestamp = DateTime.UtcNow,
remainingTime = GetRemainingTimeInCurrentWindow()
},
Message = isValid ? "Code verified successfully" : "Invalid or expired code"
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> DisableTwoFA(string secretKey, string verificationCode)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(verificationCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required to disable 2FA"
};
}
// Require valid code to disable 2FA for security
var isValid = ValidateTotpCode(secretKey, verificationCode);
if (!isValid)
{
return new ApiResponse
{
StatusCode = 400,
Message = "Invalid verification code. Cannot disable 2FA without valid code."
};
}
return new ApiResponse
{
StatusCode = 200,
Data = new
{
disabled = true,
disabledAt = DateTime.UtcNow
},
Message = "2FA has been disabled successfully"
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
private string[] GenerateBackupCodes(int count = 10)
{
var codes = new string[count];
var random = new Random();
for (int i = 0; i < count; i++)
{
codes[i] = random.Next(100000, 999999).ToString();
}
return codes;
}
private int GetRemainingTimeInCurrentWindow()
{
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingSeconds = timeWindow - (currentTime % timeWindow);
return (int)remainingSeconds;
}
private byte[] Base32Decode(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("Input cannot be null or empty");
input = input.TrimEnd('=').ToUpper();
var output = new List<byte>();
var bits = 0;
var bitsCount = 0;
foreach (char c in input)
{
var value = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".IndexOf(c);
if (value < 0)
throw new ArgumentException($"Invalid character in Base32 string: {c}");
bits = (bits << 5) | value;
bitsCount += 5;
if (bitsCount >= 8)
{
output.Add((byte)(bits >> (bitsCount - 8)));
bitsCount -= 8;
}
}
return output.ToArray();
}
private string FormatSecretForManualEntry(string secret)
{
return string.Join(" ", Enumerable.Range(0, secret.Length / 4)
.Select(i => secret.Substring(i * 4, 4)));
}
}
}