568 lines
20 KiB
C#
568 lines
20 KiB
C#
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)));
|
|
}
|
|
|
|
}
|
|
|
|
}
|