Compare commits

...

16 Commits

Author SHA1 Message Date
Developer 02
3688373481 fix(_Layout): sanitzer entfernt, um Json-Deserilisationsfehler zu vermeiden. 2025-02-11 13:43:01 +01:00
Developer 02
b8fbeee322 fix(HomeController): Zugehörige Datenbindungen hinzugefügt 2025-02-10 17:17:11 +01:00
Developer 02
57e4dfb3fb feat(EnvelopeLocked): Link zum Senden von E-Mails zu LockedFooterBodyAccess hinzugefügt.
- Gebunden mit Modell.
2025-02-10 16:58:57 +01:00
Developer 02
afc8d3baf0 chore: Upgegradet auf 2.10.4
- Resx-Felder aktualisiert.
2025-02-10 15:23:51 +01:00
Developer 02
51d77367ca fix(DIExtensions): aktualisiert, um den richtigen Abschnittsnamen einzugeben.
- ConfigureByTypeName entfernt
2025-02-10 15:05:46 +01:00
Developer 02
614f3768d9 chore(EnvelopeGenerator): Hochgestuft auf 2.10.3 2025-02-10 13:08:57 +01:00
Developer 02
5f780f8d1e fix(EnvelopeSmsHandler): Korrekte Ablaufprüfung und Cache-Aktualisierung in SendTotpAsync
- Die Bedingung für die Überprüfung des Ablaufs wurde korrigiert, so dass sie korrekt null zurückgibt, wenn der gespeicherte Ablauf in der Zukunft liegt.
- Fehlende Cache-Aktualisierung zur Speicherung des neuen Verfallsdatums nach dem Versand der TOTP-SMS hinzugefügt.
2025-02-10 11:48:36 +01:00
Developer 02
20825aa3ea feat(HomeController): Rollenprüfung für 2FA hinzugefügt
- wenn der Benutzer keine PreAuth Rolle hat, wird Status401Unauthorized zurückgegeben
2025-02-10 11:18:54 +01:00
Developer 02
c5b508d274 chore(EnvelopeGenerator): Hochgestuft auf 2.10.2 2025-02-07 14:49:39 +01:00
Developer 02
4eec4451b2 feat(TFARegController): Authentifizierungsbedingung zum Registrierungsendpunkt hinzugefügt 2025-02-07 13:31:54 +01:00
Developer 02
ca4718e159 feat(ControllerBaseExtensions): Erstellte Erweiterungsmethode zum Login über HttpContext mit Umschlag Empfänger und Rolle.
- Implementiert in HomeController
2025-02-07 13:12:27 +01:00
Developer 02
33fcb5b70e refactor(Controllers): FullyAuth-Rollenbedingung für jedes bestehende Auth-Attribut hinzugefügt, um die Autorisierung in Stufen aufzuteilen. 2025-02-07 10:53:17 +01:00
Developer 02
82d8521a25 feat(Constants): Erstellen von Konstanten für die Empfängerrolle, um die Authentifizierungsschritte des Empfängers zu trennen 2025-02-07 09:47:32 +01:00
Developer 02
2f9d07312b chore(Web): Hochgestuft auf 2.10.0 2025-02-06 19:41:47 +01:00
Developer 02
fa36593b26 refactor(Receiver): Entfernt TotpExpiration aus allen DTOs und Entitäten. 2025-02-06 19:41:11 +01:00
Developer 02
9cdb1409c0 feat(TFARegController): Try-Catch zur Methode reg'e hinzugefügt.
- Ausnahme ist so eingestellt, dass sie protokolliert wird.
2025-02-06 19:31:50 +01:00
20 changed files with 204 additions and 181 deletions

View File

@@ -4,7 +4,7 @@ using System.Text;
namespace EnvelopeGenerator.Application.DTOs.Receiver
{
public record ReceiverCreateDto([EmailAddress] string EmailAddress, string? TotpSecretkey = null, DateTime? TotpExpiration = null)
public record ReceiverCreateDto([EmailAddress] string EmailAddress, string? TotpSecretkey = null)
{
public string Signature => sha256HexOfMail.Value;

View File

@@ -20,8 +20,5 @@ public record ReceiverReadDto(
public string? TotpSecretkey { get; set; } = null;
[TemplatePlaceholder("[TFA_QR_EXPIRATION]")]
public DateTime? TotpExpiration { get; set; } = null;
public DateTime? TfaRegDeadline { get; set; }
};

View File

@@ -2,4 +2,4 @@
namespace EnvelopeGenerator.Application.DTOs.Receiver;
public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TotpExpiration = null, DateTime? TfaRegDeadline = null) : IUnique<int>;
public record ReceiverUpdateDto(int Id, string? TotpSecretkey = null, DateTime? TfaRegDeadline = null) : IUnique<int>;

View File

@@ -11,64 +11,59 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using DigitalData.Core.Client;
using QRCoder;
namespace EnvelopeGenerator.Application.Extensions
namespace EnvelopeGenerator.Application.Extensions;
public static class DIExtensions
{
public static class DIExtensions
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config)
{
public static IServiceCollection AddEnvelopeGenerator(this IServiceCollection services, IConfiguration config)
{
//Inject CRUD Service and repositoriesad
services.TryAddScoped<IConfigRepository, ConfigRepository>();
services.TryAddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
services.TryAddScoped<IEnvelopeDocumentRepository, EnvelopeDocumentRepository>();
services.TryAddScoped<IConfigRepository, ConfigRepository>();
services.TryAddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
services.TryAddScoped<IDocumentStatusRepository, DocumentStatusRepository>();
services.TryAddScoped<IEmailTemplateRepository, EmailTemplateRepository>();
services.TryAddScoped<IEnvelopeRepository, EnvelopeRepository>();
services.TryAddScoped<IEnvelopeCertificateRepository, EnvelopeCertificateRepository>();
services.TryAddScoped<IEnvelopeDocumentRepository, EnvelopeDocumentRepository>();
services.TryAddScoped<IEnvelopeHistoryRepository, EnvelopeHistoryRepository>();
services.TryAddScoped<IEnvelopeReceiverRepository, EnvelopeReceiverRepository>();
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
services.TryAddScoped<IUserReceiverRepository, UserReceiverRepository>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
services.TryAddScoped<IConfigService, ConfigService>();
services.TryAddScoped<IDocumentReceiverElementService, DocumentReceiverElementService>();
services.TryAddScoped<IEnvelopeDocumentService, EnvelopeDocumentService>();
services.TryAddScoped<IEnvelopeHistoryService, EnvelopeHistoryService>();
services.TryAddScoped<IDocumentStatusService, DocumentStatusService>();
services.TryAddScoped<IEmailTemplateService, EmailTemplateService>();
services.TryAddScoped<IEnvelopeService, EnvelopeService>();
services.TryAddScoped<IEnvelopeCertificateService, EnvelopeCertificateService>();
services.TryAddScoped<IEnvelopeDocumentService, EnvelopeDocumentService>();
services.TryAddScoped<IEnvelopeReceiverService, EnvelopeReceiverService>();
services.TryAddScoped<IEnvelopeTypeService, EnvelopeTypeService>();
services.TryAddScoped<IReceiverService, ReceiverService>();
services.TryAddScoped<IUserReceiverService, UserReceiverService>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyService, EnvelopeReceiverReadOnlyService>();
//Inject CRUD Service and repositoriesad
services.TryAddScoped<IConfigRepository, ConfigRepository>();
services.TryAddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
services.TryAddScoped<IEnvelopeDocumentRepository, EnvelopeDocumentRepository>();
services.TryAddScoped<IConfigRepository, ConfigRepository>();
services.TryAddScoped<IDocumentReceiverElementRepository, DocumentReceiverElementRepository>();
services.TryAddScoped<IDocumentStatusRepository, DocumentStatusRepository>();
services.TryAddScoped<IEmailTemplateRepository, EmailTemplateRepository>();
services.TryAddScoped<IEnvelopeRepository, EnvelopeRepository>();
services.TryAddScoped<IEnvelopeCertificateRepository, EnvelopeCertificateRepository>();
services.TryAddScoped<IEnvelopeDocumentRepository, EnvelopeDocumentRepository>();
services.TryAddScoped<IEnvelopeHistoryRepository, EnvelopeHistoryRepository>();
services.TryAddScoped<IEnvelopeReceiverRepository, EnvelopeReceiverRepository>();
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
services.TryAddScoped<IUserReceiverRepository, UserReceiverRepository>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
services.TryAddScoped<IConfigService, ConfigService>();
services.TryAddScoped<IDocumentReceiverElementService, DocumentReceiverElementService>();
services.TryAddScoped<IEnvelopeDocumentService, EnvelopeDocumentService>();
services.TryAddScoped<IEnvelopeHistoryService, EnvelopeHistoryService>();
services.TryAddScoped<IDocumentStatusService, DocumentStatusService>();
services.TryAddScoped<IEmailTemplateService, EmailTemplateService>();
services.TryAddScoped<IEnvelopeService, EnvelopeService>();
services.TryAddScoped<IEnvelopeCertificateService, EnvelopeCertificateService>();
services.TryAddScoped<IEnvelopeDocumentService, EnvelopeDocumentService>();
services.TryAddScoped<IEnvelopeReceiverService, EnvelopeReceiverService>();
services.TryAddScoped<IEnvelopeTypeService, EnvelopeTypeService>();
services.TryAddScoped<IReceiverService, ReceiverService>();
services.TryAddScoped<IUserReceiverService, UserReceiverService>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyService, EnvelopeReceiverReadOnlyService>();
//Auto mapping profiles
services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly);
services.AddAutoMapper(typeof(UserMappingProfile).Assembly);
//Auto mapping profiles
services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly);
services.AddAutoMapper(typeof(UserMappingProfile).Assembly);
services.ConfigureByTypeName<DispatcherParams>(config);
services.ConfigureByTypeName<MailParams>(config);
services.ConfigureByTypeName<AuthenticatorParams>(config);
services.ConfigureByTypeName<TotpSmsParams>(config);
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>();
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
services.TryAddSingleton<IAuthenticator, Authenticator>();
services.TryAddSingleton<QRCodeGenerator>();
return services;
}
//TODO: move to DigitalData.Core
private static IServiceCollection ConfigureByTypeName<TOptions>(this IServiceCollection services, IConfiguration configuration) where TOptions : class
=> services.Configure<TOptions>(configuration.GetSection(nameof(TOptions)));
return services;
}
}

View File

@@ -1,22 +0,0 @@
using EnvelopeGenerator.Application.DTOs.Receiver;
using EnvelopeGenerator.Extensions;
using Newtonsoft.Json;
namespace EnvelopeGenerator.Application.Extensions
{
public static class DTOExtensions
{
public static bool IsTotpSecretExpired(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
=> dto.TotpExpiration < DateTime.Now.AddMinutes(minutesBeforeExpiration * -1);
public static bool IsTotpSecretInvalid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
=> dto.IsTotpSecretExpired(minutesBeforeExpiration) || dto.TotpSecretkey is null;
public static bool IsTotpSecretValid(this ReceiverReadDto dto, int minutesBeforeExpiration = 30)
=> !dto.IsTotpSecretInvalid(minutesBeforeExpiration);
public static bool IsTotpValid(this ReceiverReadDto dto, string totp) => dto.TotpSecretkey is null ? throw new ArgumentNullException(nameof(dto), $"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(dto)}") : totp.IsValidTotp(dto.TotpSecretkey);
public static bool IsTotpInvalid(this ReceiverReadDto dto, string totp) => !dto.IsTotpValid(totp: totp);
}
}

View File

@@ -163,7 +163,7 @@
<value>Bitte überprüfen Sie die Standortinformationen. Wenn sie falsch sind, korrigieren Sie diese bitte.</value>
</data>
<data name="LockedBodyAccess" xml:space="preserve">
<value>Wir haben Ihnen gerade den Zugriffscode an die hinterlegte Email Adresse gesendet. Dies kann evtl. einige Minuten dauern.</value>
<value>Wir senden Ihnen nun einen Zugriffscode an Ihre hinterlegte Email-Adresse. Dies kann evtl. einige Minuten dauern!</value>
</data>
<data name="LockedBodyAuthenticator" xml:space="preserve">
<value>Bitte geben Sie den in Ihrer Authenticator-App angegebenen TOTP-Code ein.</value>
@@ -184,7 +184,7 @@
<value>SMS-Code</value>
</data>
<data name="LockedFooterBodyAccess" xml:space="preserve">
<value>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</value>
<value>Bitte überprüfen Sie Ihr Email Postfach inklusive Spam-Ordner. Sie können auch den Absender &lt;a class="mail-link" href="mailto:{0}?subject={1}&amp;body={2}" target="_blank"&gt;{0}&lt;/a&gt; bitten, Ihnen den Code auf anderem Wege zukommen zu lassen.</value>
</data>
<data name="LockedFooterBodyAuthenticator" xml:space="preserve">
<value>Der neue QR-Code wird nur einmal für einen bestimmten Zeitraum gesendet und nach dem Scannen in Ihrer Authenticator-App gespeichert. Er kann für alle Umschläge verwendet werden, die an dieselbe E-Mail-Adresse gesendet werden, bis er abläuft. Wenn Sie die QR-Code-Mail nicht erhalten oder sie sowohl aus der Mail als auch aus authenticator löschen, kontaktieren Sie bitte den Absender.</value>

View File

@@ -163,7 +163,7 @@
<value>Please review the location information. If it is incorrect, kindly make the necessary corrections.</value>
</data>
<data name="LockedBodyAccess" xml:space="preserve">
<value>We have just sent you the access code to the email address you provided. This may take a few minutes.</value>
<value>We will now send you an access code to your registered e-mail address. This may take a few minutes!</value>
</data>
<data name="LockedBodyAuthenticator" xml:space="preserve">
<value>Please enter the TOTP provided in your Authenticator app.</value>
@@ -184,7 +184,7 @@
<value>SMS Code</value>
</data>
<data name="LockedFooterBodyAccess" xml:space="preserve">
<value>Please check your email inbox including your spam folder. Furthermore, you can also ask the sender to send the code by other means.</value>
<value>Please check your email inbox including the spam folder. You can also ask the sender &lt;a class="mail-link" href="mailto:{0}?subject={1}&amp;body={2}" target="_blank"&gt;{0}&lt;/a&gt; to send you the code by other means.</value>
</data>
<data name="LockedFooterBodyAuthenticator" xml:space="preserve">
<value>The new QR code is sent only once for a given period and is saved in your authenticator app once scanned. It can be used for all envelopes received at the same email address until it expires. If you do not receive the QR code mail or delete it both from the mail and from authenticator, please contact the sender.</value>

View File

@@ -164,14 +164,11 @@ namespace EnvelopeGenerator.Application.Services
throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver information is missing. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}");
if (dto.Receiver.TotpSecretkey is null)
throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpSecretKey is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}");
if (dto.Receiver.TotpExpiration is null)
throw new ArgumentNullException(nameof(dto), $"TFA Qr Code cannot sent. Receiver.TotpExpiration is null. Envelope receiver dto is {JsonConvert.SerializeObject(dto)}");
var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: dto.Receiver.EmailAddress, secretKey: dto.Receiver.TotpSecretkey).ToBase64String();
return SendAsync(dto, EmailTemplateType.TotpSecret, new()
{
{"[TFA_QR_CODE]", totp_qr_64 },
{"[TFA_EXPIRATION]", dto.Receiver.TotpExpiration }
});
}
}

View File

@@ -37,13 +37,14 @@ public class EnvelopeSmsHandler : IEnvelopeSmsHandler
var key = string.Format(_totpSmsParams.Expiration.CacheKeyFormat, er_secret.EnvelopeId, er_secret.ReceiverId);
var expiration = await _dCache.GetDateTimeAsync(key, cToken);
if(expiration is DateTime expirationDateTime && expirationDateTime < DateTime.Now)
if(expiration is DateTime expirationDateTime && expirationDateTime >= DateTime.Now)
return (null, expirationDateTime);
else
{
var new_expiration = DateTime.Now.AddSeconds(_totpSmsParams.TotpStep);
var totp = _authenticator.GenerateTotp(er_secret.Receiver!.TotpSecretkey!, _totpSmsParams.TotpStep);
var msg = string.Format(_totpSmsParams.Format, totp, new_expiration.ToString(_totpSmsParams.Expiration.Format, _totpSmsParams.Expiration.CultureInfo));
await _dCache.SetDateTimeAsync(key, new_expiration, cToken: cToken);
return (await _sender.SendSmsAsync(er_secret.PhoneNumber!, msg), new_expiration);
}
}

View File

@@ -112,6 +112,13 @@
End Enum
#End Region
#Region "Role"
Public NotInheritable Class ReceiverRole
Public Const PreAuth As String = "PreAuth"
Public Const FullyAuth As String = "FullyAuth"
End Class
#End Region
#Region "Constants"
Public Const DATABASE = "DATABASE"

View File

@@ -27,9 +27,6 @@ public class Receiver : IUnique<int>
[Column("TOTP_SECRET_KEY", TypeName = "nvarchar(MAX)")]
public string? TotpSecretkey { get; set; }
[Column("TOTP_EXPIRATION", TypeName = "datetime")]
public DateTime? TotpExpiration { get; set; }
[Column("TFA_REG_DEADLINE", TypeName = "datetime")]
public DateTime? TfaRegDeadline { get; set; }

View File

@@ -1,4 +1,7 @@
using EnvelopeGenerator.Web.Models;
using EnvelopeGenerator.Application.DTOs.EnvelopeReceiver;
using EnvelopeGenerator.Web.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
@@ -6,6 +9,7 @@ namespace EnvelopeGenerator.Web.Controllers
{
public static class ControllerBaseExtensions
{
#region Auth
public static string? GetClaimValue(this ControllerBase controller, string claimType) => controller.User.FindFirstValue(claimType);
public static string? GetAuthEnvelopeUuid(this ControllerBase controller) => controller.User.FindFirstValue(ClaimTypes.NameIdentifier);
@@ -23,7 +27,35 @@ namespace EnvelopeGenerator.Web.Controllers
var env_id_str = controller.User.FindFirstValue(EnvelopeClaimTypes.Id);
return int.TryParse(env_id_str, out int env_id) ? env_id : null;
}
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto er, string receiverRole)
{
var claims = new List<Claim> {
new(ClaimTypes.NameIdentifier, er.Envelope!.Uuid),
new(ClaimTypes.Hash, er.Receiver!.Signature),
new(ClaimTypes.Name, er.Name ?? string.Empty),
new(ClaimTypes.Email, er.Receiver.EmailAddress),
new(EnvelopeClaimTypes.Title, er.Envelope.Title),
new(EnvelopeClaimTypes.Id, er.Envelope.Id.ToString()),
new(ClaimTypes.Role, receiverRole)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = false
};
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
#endregion
#region View error
//TODO: integrate localizer for ready-to-use views
public static ViewResult ViewError(this Controller controller, ErrorViewModel errorViewModel) => controller.View("_Error", errorViewModel);
@@ -61,5 +93,6 @@ namespace EnvelopeGenerator.Web.Controllers
Subtitle = "Ein unerwarteter Fehler ist aufgetreten",
Body = "Bitte kontaktieren Sie das IT-Team."
});
}
#endregion
}
}

View File

@@ -3,12 +3,12 @@ using EnvelopeGenerator.Common;
using EnvelopeGenerator.Web.Services;
using EnvelopeGenerator.Application.Contracts;
using Microsoft.AspNetCore.Authorization;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Extensions;
using static EnvelopeGenerator.Common.Constants;
namespace EnvelopeGenerator.Web.Controllers
{
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Route("api/[controller]")]
public class DocumentController : BaseController
{
@@ -48,7 +48,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("{envelopeKey}")]
public async Task<IActionResult> Open(string envelopeKey)
{

View File

@@ -10,7 +10,7 @@ using EnvelopeGenerator.Extensions;
namespace EnvelopeGenerator.Web.Controllers
{
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class EnvelopeController : BaseController
@@ -64,7 +64,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("{envelopeKey}")]
public async Task<IActionResult> Update(string envelopeKey, int index)
{
@@ -110,7 +110,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost("reject")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)
{

View File

@@ -19,7 +19,7 @@ using Ganss.Xss;
using Newtonsoft.Json;
using EnvelopeGenerator.Application.DTOs;
using DigitalData.Core.Client;
using EnvelopeGenerator.Application.Extensions;
using OtpNet;
namespace EnvelopeGenerator.Web.Controllers;
@@ -137,7 +137,9 @@ public class HomeController : ViewControllerBase
Success: er => View()
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er.HasPhoneNumber),
.WithData("HasPhoneNumber", er.HasPhoneNumber)
.WithData("SenderEmail", er.Envelope.User!.Email)
.WithData("EnvelopeTitle", er.Envelope.Title),
Fail: IActionResult (messages, notices) =>
{
_logger.LogNotice(notices);
@@ -160,7 +162,13 @@ public class HomeController : ViewControllerBase
{
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(er_secret);
if (smsRes is not null && smsRes.Failed)
ViewData["EnvelopeKey"] = envelopeReceiverId;
ViewData["TFAEnabled"] = er_secret.Envelope!.TFAEnabled;
ViewData["HasPhoneNumber"] = er_secret.HasPhoneNumber;
ViewData["SenderEmail"] = er_secret.Envelope.User!.Email;
ViewData["EnvelopeTitle"] = er_secret.Envelope.Title;
if (smsRes?.Failed ?? false)
{
var res_json = JsonConvert.SerializeObject(smsRes);
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: $"An unexpected error occurred while sending an SMS code. Response: ${res_json}");
@@ -173,7 +181,6 @@ public class HomeController : ViewControllerBase
{
return View("EnvelopeLocked")
.WithData("CodeType", "authenticatorCode")
.WithData("QRCodeExpiration", er_secret.Receiver?.TotpExpiration)
.WithData("TfaRegDeadline", er_secret.Receiver?.TfaRegDeadline);
}
}
@@ -188,6 +195,11 @@ public class HomeController : ViewControllerBase
await _historyService.RecordAsync(er_secret.EnvelopeId, er_secret.Receiver!.EmailAddress, EnvelopeStatus.AccessCodeIncorrect);
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
}
@@ -197,13 +209,14 @@ public class HomeController : ViewControllerBase
if (er_secret.Envelope!.TFAEnabled)
{
var rcv = er_secret.Receiver;
if (rcv.IsTotpSecretInvalid())
if (rcv.TotpSecretkey is null)
{
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
rcv.TotpExpiration = DateTime.Now.AddMonths(1);
await _rcvService.UpdateAsync(rcv);
await _mailService.SendTFAQrCodeAsync(er_secret);
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.PreAuth);
return await TFAViewAsync(auth.UserSelectSMS, er_secret, envelopeReceiverId);
}
@@ -216,7 +229,7 @@ public class HomeController : ViewControllerBase
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
if (!User.IsInRole(ReceiverRole.PreAuth) || !_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
@@ -229,7 +242,10 @@ public class HomeController : ViewControllerBase
[NonAction]
private async Task<IActionResult?> HandleAuthenticatorAsync(Auth auth, EnvelopeReceiverSecretDto er_secret, string envelopeReceiverId)
{
if (er_secret.Receiver!.IsTotpInvalid(totp: auth.AuthenticatorCode!))
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_authenticator.VerifyTotp(auth.AuthenticatorCode!, er_secret.Receiver.TotpSecretkey, window: VerificationWindow.RfcSpecifiedNetworkDelay))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer[WebKey.WrongAccessCode].Value;
@@ -273,23 +289,32 @@ public class HomeController : ViewControllerBase
if (auth.HasMulti)
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
return Unauthorized();
}
else if (auth.HasAccessCode)
if(await HandleAccessCodeAsync(auth, er_secret, envelopeReceiverId) is IActionResult acView)
{
if (await HandleAccessCodeAsync(auth, er_secret, envelopeReceiverId) is IActionResult acView)
return acView;
}
else if (auth.HasSmsCode)
if(await HandleSmsAsync(auth, er_secret, envelopeReceiverId) is IActionResult smsView)
{
if (await HandleSmsAsync(auth, er_secret, envelopeReceiverId) is IActionResult smsView)
return smsView;
}
else if (auth.HasAuthenticatorCode)
{
if(await HandleAuthenticatorAsync(auth, er_secret, envelopeReceiverId) is IActionResult aView)
return aView;
}
else
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
return View("EnvelopeLocked")
.WithData("EnvelopeKey", envelopeReceiverId)
.WithData("TFAEnabled", er_secret.Envelope!.TFAEnabled)
.WithData("HasPhoneNumber", er_secret.HasPhoneNumber)
.WithData("SenderEmail", er_secret.Envelope.User!.Email)
.WithData("EnvelopeTitle", er_secret.Envelope.Title)
.WithData("ErrorMessage", _localizer[WebKey.WrongAccessCode].Value);
}
@@ -317,27 +342,8 @@ public class HomeController : ViewControllerBase
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, message: "No document byte-data was found in ENVELOPE_DOCUMENT table.");
return this.ViewDocumentNotFound();
}
var claims = new List<Claim> {
new(ClaimTypes.NameIdentifier, uuid),
new(ClaimTypes.Hash, signature),
new(ClaimTypes.Name, er.Name ?? string.Empty),
new(ClaimTypes.Email, er.Receiver.EmailAddress),
new(EnvelopeClaimTypes.Title, er.Envelope.Title),
new(EnvelopeClaimTypes.Id, er.Envelope.Id.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = false
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
@@ -350,8 +356,8 @@ public class HomeController : ViewControllerBase
return this.ViewInnerServiceError();
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpGet("EnvelopeKey/{envelopeReceiverId}/Success")]
public async Task<IActionResult> EnvelopeSigned(string envelopeReceiverId)
{
@@ -386,7 +392,7 @@ public class HomeController : ViewControllerBase
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpGet("EnvelopeKey/{envelopeReceiverId}/Rejected")]
public async Task<IActionResult> EnvelopeRejected(string envelopeReceiverId)
{
@@ -492,7 +498,7 @@ public class HomeController : ViewControllerBase
}
}
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpGet("IsAuthenticated")]
public IActionResult IsAuthenticated()
{

View File

@@ -4,6 +4,7 @@ using EnvelopeGenerator.Application.DTOs.EnvelopeReceiverReadOnly;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using static EnvelopeGenerator.Common.Constants;
namespace EnvelopeGenerator.Web.Controllers
{
@@ -28,7 +29,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
[HttpPost]
[Authorize]
[Authorize(Roles = ReceiverRole.FullyAuth)]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{
try

View File

@@ -8,6 +8,7 @@ using EnvelopeGenerator.Application.Resources;
using DigitalData.Core.DTO;
using EnvelopeGenerator.Application.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authorization;
namespace EnvelopeGenerator.Web.Controllers;
@@ -28,51 +29,59 @@ public class TFARegController : ViewControllerBase
_params = tfaRegParamsOptions.Value;
}
[Authorize]
[HttpGet("{envelopeReceiverId}")]
public async Task<IActionResult> Reg(string envelopeReceiverId)
{
envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId);
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
try
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]);
return Unauthorized();
}
envelopeReceiverId = _sanitizer.Sanitize(envelopeReceiverId);
(string? uuid, string? signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (uuid is null || signature is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer[WebKey.WrongEnvelopeReceiverId]);
return Unauthorized();
}
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
if (!er_secret.Envelope!.TFAEnabled)
return Unauthorized();
if (er_secret_res.IsFailed)
{
_logger.LogNotice(er_secret_res.Notices);
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
var rcv = er_secret.Receiver;
if (!er_secret.Envelope!.TFAEnabled)
return Unauthorized();
// Generate QR code as base 64
rcv!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
rcv.TotpExpiration = DateTime.Now.AddMonths(1);
await _rcvService.UpdateAsync(rcv);
var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: rcv.EmailAddress, secretKey: rcv.TotpSecretkey).ToBase64String();
var rcv = er_secret.Receiver;
// Calculate RFA registiration deadline
if(rcv.TfaRegDeadline is null)
{
rcv.TfaRegDeadline = _params.Deadline;
// Generate QR code as base 64
rcv!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _rcvService.UpdateAsync(rcv);
var totp_qr_64 = _authenticator.GenerateTotpQrCode(userEmail: rcv.EmailAddress, secretKey: rcv.TotpSecretkey).ToBase64String();
// Calculate RFA registiration deadline
if (rcv.TfaRegDeadline is null)
{
rcv.TfaRegDeadline = _params.Deadline;
await _rcvService.UpdateAsync(rcv);
}
else if (rcv.TfaRegDeadline <= DateTime.Now)
return View("_Expired");
ViewData["RegDeadline"] = rcv.TfaRegDeadline;
ViewData["TotpQR64"] = totp_qr_64;
return View();
}
catch(Exception ex)
{
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer[WebKey.UnexpectedError]);
return this.ViewInnerServiceError();
}
else if(rcv.TfaRegDeadline <= DateTime.Now)
return View("_Expired");
ViewData["RegDeadline"] = rcv.TfaRegDeadline;
ViewData["TotpQR64"] = totp_qr_64;
return View();
}
}

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>EnvelopeGenerator.Web</PackageId>
<Version>2.10.0</Version>
<Version>2.10.4</Version>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.Web</Product>
@@ -13,8 +13,8 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<AssemblyVersion>2.10.0</AssemblyVersion>
<FileVersion>2.10.0</FileVersion>
<AssemblyVersion>2.10.4</AssemblyVersion>
<FileVersion>2.10.4</FileVersion>
<Copyright>Copyright © 2024 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -2,6 +2,7 @@
@using Newtonsoft.Json
@model Auth;
@{
//TODO: Create view model
var nonce = _accessor.HttpContext?.Items["csp-nonce"] as string;
var logo = _logoOpt.Value;
ViewData["Title"] = _localizer[WebKey.DocProtected];
@@ -12,11 +13,12 @@
bool viaAuthenticator = codeType == "authenticatorCode";
bool viaTFA = viaSms || viaAuthenticator;
DateTime? smsExpiration = ViewData["SmsExpiration"] is DateTime _smsExpiration ? _smsExpiration : null;
DateTime? qrCodeExpiration = ViewData["QRCodeExpiration"] is DateTime _qrCodeExpiration ? _qrCodeExpiration : null;
bool tfaEnabled = ViewData["TFAEnabled"] is bool _tfaEnabled && _tfaEnabled;
bool hasPhoneNumber = ViewData["HasPhoneNumber"] is bool _hasPhoneNumber && _hasPhoneNumber;
var envelopeKey = ViewData["EnvelopeKey"] as string;
DateTime? tfaRegDeadline = ViewData["TfaRegDeadline"] is DateTime _deadline ? _deadline : null;
var senderEmail = ViewData["SenderEmail"] as string ?? string.Empty;
var envelopeTitle = ViewData["EnvelopeTitle"] as string ?? string.Empty;
}
<div class="page container py-4 px-4">
<header class="text-center">
@@ -32,7 +34,7 @@
</div>
<h1>@_localizer[WebKey.Formats.LockedTitle.Format(codeKeyName)]</h1>
</header>
@if (tfaRegDeadline is not null && tfaRegDeadline > DateTime.Now)
@if (viaAuthenticator && (tfaRegDeadline is null || tfaRegDeadline > DateTime.Now))
{
<section class="text-center">
<p class="m-0 p-0">
@@ -46,7 +48,7 @@
</section>
}
<section class="text-center">
<p>@_localizer[WebKey.Formats.LockedBody.Format(codeKeyName)].Value.Format(qrCodeExpiration.ToString())</p>
<p>@_localizer[WebKey.Formats.LockedBody.Format(codeKeyName)].Value</p>
</section>
<div class="row m-0 p-0">
<div class="access-code-panel justify-content-center align-items-center p-0 m-0">
@@ -91,7 +93,7 @@
<section class="no-receiver-explanation text-center">
<details>
<summary>@_localizer[WebKey.Formats.LockedFooterTitle.Format(codeKeyName)]</summary>
<p>@_localizer[WebKey.Formats.LockedFooterBody.Format(codeKeyName)]</p>
<p>@Html.Raw(_localizer[WebKey.Formats.LockedFooterBody.Format(codeKeyName)].Value.Format(senderEmail, "Envelope - " + envelopeTitle, string.Empty))</p>
</details>
</section>
</div>

View File

@@ -68,7 +68,7 @@
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var lStrsJson = JsonConvert.SerializeObject(_localizer.ToDictionary(), settings).TrySanitize(_sanitizer);
var lStrsJson = JsonConvert.SerializeObject(_localizer.ToDictionary(), settings);
}
<script nonce="@nonce">
var localized = @Html.Raw(lStrsJson)