refactor(HomeController): LogInEnvelope aktualisiert, um SMS-Code als TOTP zu verifizieren

This commit is contained in:
Developer 02 2025-01-27 13:47:26 +01:00
parent 3267acbeb3
commit af5d7c289d
11 changed files with 209 additions and 286 deletions

View File

@ -13,7 +13,5 @@
/// The placeholder {0} represents the envelopeReceiverId.
/// </summary>
public string CodeExpirationCacheKeyFormat { get; init; } = "sms-code-expiration-{0}";
public TimeSpan CodeCacheValidityPeriod { get; init; } = new(0, 5, 0);
}
}

View File

@ -1,5 +1,6 @@
using DigitalData.Core.Abstractions.Client;
using Microsoft.Extensions.Caching.Distributed;
using OtpNet;
namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
{
@ -21,5 +22,9 @@ namespace EnvelopeGenerator.Application.Configurations.GtxMessaging
public string MessageQueryParamName { get; init; } = "text";
public int CodeLength { get; init; } = 5;
public int SmsTotpStep { get; init; } = 300;
public string DefaultTotpMessageFormat { get; init; } = "{0}";
}
}

View File

@ -15,5 +15,7 @@ namespace EnvelopeGenerator.Application.Contracts
string GenerateTotp(string secretKey, int step = 30);
bool VerifyTotp(string totpCode, string secretKey, int step = 30, VerificationWindow? window = null);
bool GetTotpExpirationTime(int step = 30);
}
}

View File

@ -1,17 +0,0 @@
namespace EnvelopeGenerator.Application.Contracts
{
public interface IEnvelopeReceiverCache
{
Task<string?> GetSmsCodeAsync(string envelopeReceiverId);
/// <summary>
/// Asynchronously stores an SMS verification code in the cache and returns the expiration date of the code.
/// </summary>
/// <param name="envelopeReceiverId">The unique identifier for the recipient of the envelope to associate with the SMS code.</param>
/// <param name="code">The SMS verification code to be stored.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the expiration date and time of the stored SMS code.</returns>
Task<DateTime> SetSmsCodeAsync(string envelopeReceiverId, string code);
Task<DateTime?> GetSmsCodeExpirationAsync(string envelopeReceiverId);
}
}

View File

@ -1,13 +1,12 @@
using EnvelopeGenerator.Application.DTOs.Messaging;
namespace EnvelopeGenerator.Application.Contracts
namespace EnvelopeGenerator.Application.Contracts;
public interface IMessagingService
{
public interface IMessagingService
{
string ServiceProvider { get; }
Task<SmsResponse> SendSmsAsync(string recipient, string message);
Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId);
}
Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string messageFormat = "{0}");
}

View File

@ -4,16 +4,6 @@
{
public required bool Ok { get; init; }
public DateTime? Expiration { get; set; }
public DateTime? AllowedAt { get; set; }
public TimeSpan AllowedAfter => Allowed ? TimeSpan.Zero : AllowedAt!.Value - DateTime.Now;
public bool Allowed => AllowedAt is null || DateTime.Now >= AllowedAt;
public bool Error => !Ok && Allowed;
public dynamic? Errors { get; init; }
}
}

View File

@ -67,5 +67,10 @@ namespace EnvelopeGenerator.Application.Services
public bool VerifyTotp(string totpCode, string secretKey, int step = 30, VerificationWindow? window = null)
=> new Totp(Base32Encoding.ToBytes(secretKey), step).VerifyTotp(totpCode, out _, window);
public bool GetTotpExpirationTime(int step = 30)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,50 +0,0 @@
using AngleSharp.Dom;
using EnvelopeGenerator.Application.Configurations;
using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Services
{
public class EnvelopeReceiverCache : IEnvelopeReceiverCache
{
private readonly EnvelopeReceiverCacheParams _cacheParams;
private readonly DistributedCacheEntryOptions _codeCacheOptions;
private readonly IDistributedCache _cache;
public EnvelopeReceiverCache(IOptions<EnvelopeReceiverCacheParams> cacheParamOptions, IDistributedCache cache)
{
_cacheParams = cacheParamOptions.Value;
_codeCacheOptions = new() { AbsoluteExpirationRelativeToNow = cacheParamOptions.Value.CodeCacheValidityPeriod };
_cache = cache;
}
public async Task<string?> GetSmsCodeAsync(string envelopeReceiverId)
{
var code_key = string.Format(_cacheParams.CodeCacheKeyFormat, envelopeReceiverId);
return await _cache.GetStringAsync(code_key);
}
public async Task<DateTime> SetSmsCodeAsync(string envelopeReceiverId, string code)
{
// set key
var code_key = string.Format(_cacheParams.CodeCacheKeyFormat, envelopeReceiverId);
await _cache.SetStringAsync(code_key, code, _codeCacheOptions);
// set expiration
var code_expiration_key = string.Format(_cacheParams.CodeExpirationCacheKeyFormat, envelopeReceiverId);
var expiration = DateTime.Now + _cacheParams.CodeCacheValidityPeriod;
await _cache.SetDateTimeAsync(code_expiration_key, expiration, _codeCacheOptions);
return expiration;
}
public async Task<DateTime?> GetSmsCodeExpirationAsync(string envelopeReceiverId)
{
var code_expiration_key = string.Format(_cacheParams.CodeExpirationCacheKeyFormat, envelopeReceiverId);
return await _cache.GetDateTimeAsync(code_expiration_key);
}
}
}

View File

@ -6,10 +6,10 @@ using EnvelopeGenerator.Application.Contracts;
using EnvelopeGenerator.Application.DTOs.Messaging;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Application.Services
namespace EnvelopeGenerator.Application.Services;
public class GtxMessagingService : IMessagingService
{
public class GtxMessagingService : IMessagingService
{
private readonly IHttpClientService<SmsParams> _smsClient;
private readonly SmsParams _smsParams;
@ -18,18 +18,15 @@ namespace EnvelopeGenerator.Application.Services
private readonly ICodeGenerator _codeGen;
private readonly IEnvelopeReceiverCache _erCache;
public string ServiceProvider { get; }
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator, IEnvelopeReceiverCache envelopeReceiverCache)
public GtxMessagingService(IHttpClientService<SmsParams> smsClient, IOptions<SmsParams> smsParamsOptions, IMapper mapper, ICodeGenerator codeGenerator)
{
_smsClient = smsClient;
_smsParams = smsParamsOptions.Value;
_mapper = mapper;
ServiceProvider = GetType().Name.Replace("Service", string.Empty);
_codeGen = codeGenerator;
_erCache = envelopeReceiverCache;
}
public async Task<SmsResponse> SendSmsAsync(string recipient, string message)
@ -43,25 +40,10 @@ namespace EnvelopeGenerator.Application.Services
.ThenAsync(_mapper.Map<SmsResponse>);
}
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string envelopeReceiverId)
public async Task<SmsResponse> SendSmsCodeAsync(string recipient, string secretKey, string? messageFormat = null)
{
var code = await _erCache.GetSmsCodeAsync(envelopeReceiverId);
if (code is null)
{
code = _codeGen.GenerateCode(_smsParams.CodeLength);
var expiration = await _erCache.SetSmsCodeAsync(envelopeReceiverId, code);
var res = await SendSmsAsync(recipient: recipient, message: code);
res.Expiration = expiration;
return res;
}
else
{
var code_expiration = await _erCache.GetSmsCodeExpirationAsync(envelopeReceiverId);
return code_expiration is null
? new() { Ok = false }
: new() { Ok = false, AllowedAt = code_expiration };
}
}
var code = _codeGen.GenerateTotp(secretKey, _smsParams.SmsTotpStep);
var message = string.Format(messageFormat ?? _smsParams.DefaultTotpMessageFormat, code);
return await SendSmsAsync(recipient: recipient, message: message);
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.Extensions.Caching.Distributed;
namespace EnvelopeGenerator.Extensions
{
public static class CacheExtensions
{
public static IDistributedCache Cache(this IDistributedCache cache)
{
cache.SetStringAsync()
}
}
}

View File

@ -36,11 +36,10 @@ namespace EnvelopeGenerator.Web.Controllers
private readonly IEnvelopeMailService _mailService;
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
private readonly IMessagingService _msgService;
private readonly IEnvelopeReceiverCache _erCache;
private readonly ICodeGenerator _codeGenerator;
private readonly IReceiverService _rcvService;
public HomeController(EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IMessagingService messagingService, IEnvelopeReceiverCache envelopeReceiverCache, ICodeGenerator codeGenerator, IReceiverService receiverService)
public HomeController(EnvelopeOldService envelopeOldService, ILogger<HomeController> logger, IEnvelopeReceiverService envelopeReceiverService, IEnvelopeHistoryService historyService, IStringLocalizer<Resource> localizer, IConfiguration configuration, HtmlSanitizer sanitizer, Cultures cultures, IEnvelopeMailService envelopeMailService, IEnvelopeReceiverReadOnlyService readOnlyService, IMessagingService messagingService, ICodeGenerator codeGenerator, IReceiverService receiverService)
{
this.envelopeOldService = envelopeOldService;
_envRcvService = envelopeReceiverService;
@ -53,7 +52,6 @@ namespace EnvelopeGenerator.Web.Controllers
_logger = logger;
_readOnlyService = readOnlyService;
_msgService = messagingService;
_erCache = envelopeReceiverCache;
_codeGenerator = codeGenerator;
_rcvService = receiverService;
}
@ -183,14 +181,21 @@ namespace EnvelopeGenerator.Web.Controllers
//check access code
EnvelopeResponse response = await envelopeOldService.LoadEnvelope(envelopeReceiverId);
return await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature).ThenAsync(
SuccessAsync: async er_secret =>
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
async Task<IActionResult> TFAView(bool viaSms)
{
if (viaSms)
{
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, envelopeReceiverId: envelopeReceiverId);
//add date time cache
var res = await _msgService.SendSmsCodeAsync(er_secret.PhoneNumber!, er_secret.Receiver.TotpSecretkey);
if (res.Ok)
return View("EnvelopeLocked").WithData("CodeType", "smsCode").WithData("SmsExpiration", res.Expiration);
else if (!res.Allowed)
@ -245,11 +250,10 @@ namespace EnvelopeGenerator.Web.Controllers
}
else if (auth.HasSmsCode)
{
var smsCode = await _erCache.GetSmsCodeAsync(envelopeReceiverId);
if (smsCode is null)
return RedirectToAction("EnvelopeLocked", new { envelopeReceiverId });
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if(auth.SmsCode != smsCode)
if (_codeGenerator.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey, step: 60 * 5))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
@ -323,13 +327,6 @@ namespace EnvelopeGenerator.Web.Controllers
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
return View("ShowEnvelope", er);
},
Fail: (messages, notices) =>
{
_logger.LogNotice(notices);
return this.ViewEnvelopeNotFound();
}
);
}
catch (Exception ex)
{