ini
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user