Skip to content

Commit

Permalink
Use MinimalAPI for two-factor authentication code request (#16174)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Hisham Bin Ateya <[email protected]>
  • Loading branch information
MikeAlhayek and hishamco authored May 27, 2024
1 parent 8bf9641 commit 4d10d15
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,37 +148,6 @@ public async Task<IActionResult> ValidateCode(EnableEmailAuthenticatorViewModel
return View(nameof(RequestCode), model);
}

[HttpPost, Produces("application/json"), AllowAnonymous]
public async Task<IActionResult> SendCode()
{
var user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
var errorMessage = S["The email could not be sent. Please attempt to request the code at a later time."];

if (user == null)
{
return BadRequest(new
{
success = false,
message = errorMessage.Value,
});
}

var settings = (await SiteService.GetSiteSettingsAsync()).As<EmailAuthenticatorLoginSettings>();
var code = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);

var to = await UserManager.GetEmailAsync(user);
var subject = await GetSubjectAsync(settings, user, code);
var body = await GetBodyAsync(settings, user, code);
var result = await _emailService.SendAsync(to, subject, body);

return Ok(new
{
success = result.Succeeded,
message = result.Succeeded ? S["A verification code has been sent via email. Please check your email for the code."].Value
: errorMessage.Value,
});
}

private Task<string> GetSubjectAsync(EmailAuthenticatorLoginSettings settings, IUser user, string code)
{
var message = string.IsNullOrWhiteSpace(settings.Subject)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid.Values;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using OrchardCore.Email;
using OrchardCore.Liquid;
using OrchardCore.Settings;
using OrchardCore.Users.Models;

namespace OrchardCore.Users.Endpoints.EmailAuthenticator;

public static class SendCode
{
public const string RouteName = "EmailAuthenticatorSendCode";

public static IEndpointRouteBuilder AddEmailSendCodeEndpoint<T>(this IEndpointRouteBuilder builder)
{
builder.MapPost("TwoFactor-Authenticator/EmailSendCode", HandleAsync<T>)
.AllowAnonymous()
.WithName(RouteName)
.DisableAntiforgery();

return builder;
}

private static async Task<IResult> HandleAsync<T>(
SignInManager<IUser> signInManager,
UserManager<IUser> userManager,
ISiteService siteService,
IEmailService emailService,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder,
IStringLocalizer<T> S)
{
var user = await signInManager.GetTwoFactorAuthenticationUserAsync();
var errorMessage = S["The email could not be sent. Please attempt to request the code at a later time."];

if (user == null)
{
return TypedResults.BadRequest(new
{
success = false,
message = errorMessage.Value,
});
}

var settings = (await siteService.GetSiteSettingsAsync()).As<EmailAuthenticatorLoginSettings>();
var code = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);

var to = await userManager.GetEmailAsync(user);
var subject = await GetSubjectAsync(settings, user, code, liquidTemplateManager, htmlEncoder);
var body = await GetBodyAsync(settings, user, code, liquidTemplateManager, htmlEncoder);
var result = await emailService.SendAsync(to, subject, body);

return TypedResults.Ok(new
{
success = result.Succeeded,
message = result.Succeeded
? S["A verification code has been sent via email. Please check your email for the code."].Value
: errorMessage.Value,
});
}

private static Task<string> GetSubjectAsync(
EmailAuthenticatorLoginSettings settings,
IUser user,
string code,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder)
{
var message = string.IsNullOrWhiteSpace(settings.Subject)
? EmailAuthenticatorLoginSettings.DefaultSubject
: settings.Subject;

return GetContentAsync(message, user, code, liquidTemplateManager, htmlEncoder);
}

private static Task<string> GetBodyAsync(
EmailAuthenticatorLoginSettings settings,
IUser user,
string code,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder)
{
var message = string.IsNullOrWhiteSpace(settings.Body)
? EmailAuthenticatorLoginSettings.DefaultBody
: settings.Body;

return GetContentAsync(message, user, code, liquidTemplateManager, htmlEncoder);
}

private static async Task<string> GetContentAsync(
string message,
IUser user,
string code,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder)
{
var result = await liquidTemplateManager.RenderHtmlContentAsync(message, htmlEncoder, null,
new Dictionary<string, FluidValue>()
{
["User"] = new ObjectValue(user),
["Code"] = new StringValue(code),
});

using var writer = new StringWriter();
result.WriteTo(writer, htmlEncoder);

return writer.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid.Values;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.Liquid;
using OrchardCore.Settings;
using OrchardCore.Sms;
using OrchardCore.Users.Models;

namespace OrchardCore.Users.Endpoints.SmsAuthenticator;

public static class SendCode
{
public const string RouteName = "SmsAuthenticatorSendCode";

public static IEndpointRouteBuilder AddSmsSendCodeEndpoint<T>(this IEndpointRouteBuilder builder)
{
builder.MapPost("TwoFactor-Authenticator/SmsSendCode", HandleAsync<T>)
.AllowAnonymous()
.WithName(RouteName)
.DisableAntiforgery();

return builder;
}

private static async Task<IResult> HandleAsync<T>(
SignInManager<IUser> signInManager,
UserManager<IUser> userManager,
ISiteService siteService,
ISmsService smsService,
IOptions<IdentityOptions> identityOptions,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder,
IStringLocalizer<T> S)
{
var user = await signInManager.GetTwoFactorAuthenticationUserAsync();
var errorMessage = S["The SMS message could not be sent. Please attempt to request the code at a later time."];

if (user == null)
{
return TypedResults.BadRequest(new
{
success = false,
message = errorMessage.Value,
});
}

var settings = (await siteService.GetSiteSettingsAsync()).As<SmsAuthenticatorLoginSettings>();
var code = await userManager.GenerateTwoFactorTokenAsync(user, identityOptions.Value.Tokens.ChangePhoneNumberTokenProvider);

var message = new SmsMessage()
{
To = await userManager.GetPhoneNumberAsync(user),
Body = await GetBodyAsync(settings, user, code, liquidTemplateManager, htmlEncoder),
};

var result = await smsService.SendAsync(message);

return TypedResults.Ok(new
{
success = result.Succeeded,
message = result.Succeeded ? S["A verification code has been sent to your phone number. Please check your device for the code."].Value
: errorMessage.Value,
});
}

private static Task<string> GetBodyAsync(
SmsAuthenticatorLoginSettings settings,
IUser user,
string code,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder)
{
var message = string.IsNullOrWhiteSpace(settings.Body)
? EmailAuthenticatorLoginSettings.DefaultBody
: settings.Body;

return GetContentAsync(message, user, code, liquidTemplateManager, htmlEncoder);
}

private static async Task<string> GetContentAsync(
string message,
IUser user,
string code,
ILiquidTemplateManager liquidTemplateManager,
HtmlEncoder htmlEncoder)
{
var result = await liquidTemplateManager.RenderHtmlContentAsync(message, htmlEncoder, null,
new Dictionary<string, FluidValue>()
{
["User"] = new ObjectValue(user),
["Code"] = new StringValue(code),
});

using var writer = new StringWriter();
result.WriteTo(writer, htmlEncoder);

return writer.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using OrchardCore.Settings;
using OrchardCore.Users.Controllers;
using OrchardCore.Users.Drivers;
using OrchardCore.Users.Endpoints.EmailAuthenticator;
using OrchardCore.Users.Endpoints.SmsAuthenticator;
using OrchardCore.Users.Events;
using OrchardCore.Users.Filters;
using OrchardCore.Users.Models;
Expand Down Expand Up @@ -43,17 +45,25 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro
_userOptions ??= serviceProvider.GetRequiredService<IOptions<UserOptions>>().Value;

routes.MapAreaControllerRoute(
name: "LoginWithTwoFactorAuthentication",
areaName: UserConstants.Features.Users,
pattern: "LoginWithTwoFactorAuthentication",
defaults: new { controller = _twoFactorControllerName, action = nameof(TwoFactorAuthenticationController.LoginWithTwoFactorAuthentication) }
);
name: "LoginWithTwoFactorAuthentication",
areaName: UserConstants.Features.Users,
pattern: "LoginWithTwoFactorAuthentication",
defaults: new
{
controller = _twoFactorControllerName,
action = nameof(TwoFactorAuthenticationController.LoginWithTwoFactorAuthentication),
}
);

routes.MapAreaControllerRoute(
name: "TwoFactorAuthentication",
areaName: UserConstants.Features.Users,
pattern: _userOptions.TwoFactorAuthenticationPath,
defaults: new { controller = _twoFactorControllerName, action = nameof(TwoFactorAuthenticationController.Index) }
defaults: new
{
controller = _twoFactorControllerName,
action = nameof(TwoFactorAuthenticationController.Index),
}
);
}
}
Expand Down Expand Up @@ -105,6 +115,11 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<IDisplayDriver<TwoFactorMethod>, TwoFactorMethodLoginEmailDisplayDriver>();
services.AddScoped<IDisplayDriver<ISite>, EmailAuthenticatorLoginSettingsDisplayDriver>();
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
routes.AddEmailSendCodeEndpoint<EmailAuthenticatorStartup>();
}
}

[Feature(UserConstants.Features.SmsAuthenticator)]
Expand All @@ -124,4 +139,9 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<IDisplayDriver<TwoFactorMethod>, TwoFactorMethodLoginSmsDisplayDriver>();
services.AddScoped<IDisplayDriver<ISite>, SmsAuthenticatorLoginSettingsDisplayDriver>();
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
routes.AddSmsSendCodeEndpoint<SmsAuthenticatorStartup>();
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery AntiforgeryService
@{
var tokenSet = AntiforgeryService.GetAndStoreTokens(ViewContext.HttpContext);
}
@using OrchardCore.Users.Endpoints.EmailAuthenticator

<h5 class="text-center mb-3">@T["Please <a href=\"#\" id=\"RequestCode\">click here</a> to receive a one-time verification code via email."]</h5>

<div class="text-center alert d-none"
id="RequestCodeFeedback"
data-token-field-name="@tokenSet.FormFieldName"
data-token-endpoint="@Url.ActionLink("SendCode", "EmailAuthenticator")"
data-antiforgery-token="@tokenSet.RequestToken">
</div>
<div class="text-center alert d-none" id="RequestCodeFeedback"></div>

<form id="RequestCodeForm" method="post" asp-action="SendCode" asp-controller="EmailAuthenticator" class="d-none">
<form id="RequestCodeForm" method="post" asp-area="@UserConstants.Features.Users" asp-route="@SendCode.RouteName" class="d-none">
</form>

<script at="Head" asp-name="RequestVerificationCodeViaEmail">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery AntiforgeryService
@{
var tokenSet = AntiforgeryService.GetAndStoreTokens(ViewContext.HttpContext);
}
@using OrchardCore.Users.Endpoints.SmsAuthenticator

<h5 class="text-center mb-3">@T["Please <a href=\"#\" id=\"RequestCode\">click here</a> to receive a one-time verification code via SMS."]</h5>

<div class="text-center alert d-none"
id="RequestCodeFeedback"
data-token-field-name="@tokenSet.FormFieldName"
data-token-endpoint="@Url.ActionLink("SendCode", "SmsAuthenticator")"
data-antiforgery-token="@tokenSet.RequestToken">
</div>
<div class="text-center alert d-none" id="RequestCodeFeedback"></div>

<form id="RequestCodeForm" method="post" asp-action="SendCode" asp-controller="SmsAuthenticator" class="d-none">
<form id="RequestCodeForm" method="post" asp-area="@UserConstants.Features.Users" asp-route="@SendCode.RouteName" class="d-none">
</form>

<script at="Head" asp-name="RequestVerificationCodeViaSms">
Expand Down

0 comments on commit 4d10d15

Please sign in to comment.