Skip to content

Commit

Permalink
add phone number normalization to Boilerplate (#8905)
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmoradi committed Oct 15, 2024
1 parent a039a2b commit c2d9728
Show file tree
Hide file tree
Showing 13 changed files with 55 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Bit.Bswup" Version="8.12.0-pre-03" />
<PackageVersion Include="Bit.CodeAnalyzers" Version="8.12.0-pre-03" />
<PackageVersion Include="Bit.SourceGenerators" Version="8.12.0-pre-03" />
<PackageVersion Include="libphonenumber-csharp" Version="8.13.47" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="8.0.10" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="libphonenumber-csharp" />
<PackageReference Condition=" '$(appInsights)' == 'true' OR '$(appInsights)' == '' " Include="Microsoft.ApplicationInsights.AspNetCore" />
<PackageReference Include="Humanizer" />
<PackageReference Include="QRCoder" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ namespace Boilerplate.Server.Api.Controllers.Identity;

public partial class IdentityController
{
[AutoInject] private SmsService smsService = default!;
[AutoInject] private PhoneService phoneService = default!;

[HttpPost]
public async Task SendConfirmPhoneToken(SendPhoneTokenRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindByPhoneNumber(request.PhoneNumber!)
?? throw new BadRequestException(Localizer[nameof(AppStrings.UserNotFound)]);

Expand All @@ -25,6 +26,7 @@ public async Task SendConfirmPhoneToken(SendPhoneTokenRequestDto request, Cancel
[HttpPost, Produces<TokenResponseDto>()]
public async Task ConfirmPhone(ConfirmPhoneRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindByPhoneNumber(request.PhoneNumber!)
?? throw new BadRequestException(Localizer[nameof(AppStrings.UserNotFound)]);

Expand Down Expand Up @@ -77,6 +79,6 @@ private async Task SendConfirmPhoneToken(User user, CancellationToken cancellati
var token = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"VerifyPhoneNumber:{phoneNumber},{user.PhoneNumberTokenRequestedOn?.ToUniversalTime()}"));
var link = new Uri(HttpContext.Request.GetWebClientUrl(), $"{Urls.ConfirmPage}?phoneNumber={Uri.EscapeDataString(phoneNumber!)}&phoneToken={Uri.EscapeDataString(token)}&culture={CultureInfo.CurrentUICulture.Name}");

await smsService.SendSms(Localizer[nameof(AppStrings.ConfirmPhoneTokenSmsText), token], phoneNumber, cancellationToken);
await phoneService.SendSms(Localizer[nameof(AppStrings.ConfirmPhoneTokenSmsText), token], phoneNumber, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public partial class IdentityController
[HttpPost]
public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindUserAsync(request)
?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

Expand Down Expand Up @@ -48,7 +49,7 @@ public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto reques

if (await userManager.IsPhoneNumberConfirmedAsync(user))
{
sendMessagesTasks.Add(smsService.SendSms(message, user.PhoneNumber!, cancellationToken));
sendMessagesTasks.Add(phoneService.SendSms(message, user.PhoneNumber!, cancellationToken));
}

//#if (signalr == true)
Expand All @@ -65,6 +66,7 @@ public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto reques
[HttpPost]
public async Task ResetPassword(ResetPasswordRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindUserAsync(request) ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

var expired = (DateTimeOffset.Now - user.ResetPasswordTokenRequestedOn) > AppSettings.Identity.ResetPasswordTokenLifetime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public async Task<ActionResult> SocialSignInCallback(string? returnUrl = null, i
try
{
var email = info.Principal.GetEmail();
var phoneNumber = info.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.HomePhone or ClaimTypes.MobilePhone or ClaimTypes.OtherPhone)?.Value;
var phoneNumber = phoneService.NormalizePhoneNumber(info.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.HomePhone or ClaimTypes.MobilePhone or ClaimTypes.OtherPhone)?.Value);

var user = await userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Boilerplate.Shared.Dtos.Identity;
using Boilerplate.Server.Api.Models.Identity;
using Boilerplate.Shared.Controllers.Identity;
using Boilerplate.Server.Api.Services.Identity;

namespace Boilerplate.Server.Api.Controllers.Identity;

Expand Down Expand Up @@ -43,6 +44,7 @@ public partial class IdentityController : AppControllerBase, IIdentityController
[HttpPost]
public async Task SignUp(SignUpRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
//#if (captcha == "reCaptcha")
if (await googleRecaptchaHttpClient.Verify(request.GoogleRecaptchaResponse, cancellationToken) is false)
throw new BadRequestException(Localizer[nameof(AppStrings.InvalidGoogleRecaptchaResponse)]);
Expand Down Expand Up @@ -88,6 +90,7 @@ public async Task SignUp(SignUpRequestDto request, CancellationToken cancellatio
[HttpPost, Produces<SignInResponseDto>()]
public async Task SignIn(SignInRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
signInManager.AuthenticationScheme = IdentityConstants.BearerScheme;

var user = await userManager.FindUserAsync(request) ?? throw new UnauthorizedException(Localizer[nameof(AppStrings.InvalidUserCredentials)]);
Expand Down Expand Up @@ -239,6 +242,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not U
[HttpPost]
public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null, CancellationToken cancellationToken = default)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindUserAsync(request)
?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

Expand All @@ -264,7 +268,7 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null,
if (await userManager.IsPhoneNumberConfirmedAsync(user))
{
var smsMessage = Localizer[nameof(AppStrings.OtpShortText), await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"Otp_Sms,{user.OtpRequestedOn?.ToUniversalTime()}"))].ToString();
sendMessagesTasks.Add(smsService.SendSms(smsMessage, user.PhoneNumber!, cancellationToken));
sendMessagesTasks.Add(phoneService.SendSms(smsMessage, user.PhoneNumber!, cancellationToken));
}

//#if (signalr == true)
Expand All @@ -283,6 +287,7 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null,
[HttpPost]
public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindUserAsync(request) ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

if (user.TwoFactorEnabled is false)
Expand Down Expand Up @@ -320,7 +325,7 @@ public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken

if (firstStepAuthenticationMethod != "Sms" && await userManager.IsPhoneNumberConfirmedAsync(user))
{
sendMessagesTasks.Add(smsService.SendSms(message, user.PhoneNumber!, cancellationToken));
sendMessagesTasks.Add(phoneService.SendSms(message, user.PhoneNumber!, cancellationToken));
}

//#if (signalr == true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Text;
using System.Text.Encodings.Web;
using Humanizer;
using QRCoder;
using Humanizer;
using Boilerplate.Server.Api.Services;
using Boilerplate.Shared.Dtos.Identity;
using Boilerplate.Server.Api.Models.Identity;
Expand All @@ -18,7 +18,7 @@ public partial class UserController : AppControllerBase, IUserController

[AutoInject] private IUserEmailStore<User> userEmailStore = default!;

[AutoInject] private SmsService smsService = default!;
[AutoInject] private PhoneService phoneService = default!;

[AutoInject] private EmailService emailService = default!;

Expand Down Expand Up @@ -218,6 +218,7 @@ public async Task ChangeEmail(ChangeEmailRequestDto request, CancellationToken c
[HttpPost]
public async Task SendChangePhoneNumberToken(SendPhoneTokenRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindByIdAsync(User.GetUserId().ToString());

var resendDelay = (DateTimeOffset.Now - user!.PhoneNumberTokenRequestedOn) - AppSettings.Identity.PhoneNumberTokenLifetime;
Expand All @@ -233,12 +234,13 @@ public async Task SendChangePhoneNumberToken(SendPhoneTokenRequestDto request, C

var token = await userManager.GenerateChangePhoneNumberTokenAsync(user!, request.PhoneNumber!);

await smsService.SendSms(Localizer[nameof(AppStrings.ChangePhoneNumberTokenSmsText), token], request.PhoneNumber!, cancellationToken);
await phoneService.SendSms(Localizer[nameof(AppStrings.ChangePhoneNumberTokenSmsText), token], request.PhoneNumber!, cancellationToken);
}

[HttpPost]
public async Task ChangePhoneNumber(ChangePhoneNumberRequestDto request, CancellationToken cancellationToken)
{
request.PhoneNumber = phoneService.NormalizePhoneNumber(request.PhoneNumber);
var user = await userManager.FindByIdAsync(User.GetUserId().ToString());

var expired = (DateTimeOffset.Now - user!.PhoneNumberTokenRequestedOn) > AppSettings.Identity.PhoneNumberTokenLifetime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.ResponseCompression;
using Twilio;
using PhoneNumbers;
using FluentStorage;
using FluentStorage.Blobs;
//#if (notification == true)
Expand All @@ -19,6 +20,7 @@
using Boilerplate.Server.Api.Services;
using Boilerplate.Server.Api.Controllers;
using Boilerplate.Server.Api.Models.Identity;
using Boilerplate.Server.Api.Services.Identity;

namespace Boilerplate.Server.Api;

Expand Down Expand Up @@ -195,7 +197,7 @@ void AddDbContext(DbContextOptionsBuilder options)
}

services.AddTransient<EmailService>();
services.AddTransient<SmsService>();
services.AddTransient<PhoneService>();
if (appSettings.Sms.Configured)
{
TwilioClient.Init(appSettings.Sms.TwilioAccountSid, appSettings.Sms.TwilioAutoToken);
Expand Down Expand Up @@ -230,6 +232,8 @@ void AddDbContext(DbContextOptionsBuilder options)
services.AddAdsPush(configuration);
services.AddTransient<PushNotificationService>();
//#endif

services.AddSingleton(_ => PhoneNumberUtil.GetInstance());
}

private static void AddIdentity(WebApplicationBuilder builder)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Data;

namespace Boilerplate.Server.Api.Services;
namespace Boilerplate.Server.Api.Services.Identity;

public partial class AppIdentityErrorDescriber : IdentityErrorDescriber
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication;

namespace Boilerplate.Server.Api.Services;
namespace Boilerplate.Server.Api.Services.Identity;

/// <summary>
/// Stores bearer token in jwt format
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Boilerplate.Server.Api.Models.Identity;

namespace Boilerplate.Server.Api.Services;
namespace Boilerplate.Server.Api.Services.Identity;

public partial class AppUserClaimsPrincipalFactory(UserManager<User> userManager, IOptions<IdentityOptions> optionsAccessor)
: UserClaimsPrincipalFactory<User>(userManager, optionsAccessor)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Boilerplate.Server.Api.Models.Identity;

namespace Boilerplate.Server.Api.Services;
namespace Boilerplate.Server.Api.Services.Identity;

public partial class AppUserConfirmation : IUserConfirmation<User>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
using Twilio.Rest.Api.V2010.Account;
using PhoneNumbers;
using Twilio.Rest.Api.V2010.Account;

namespace Boilerplate.Server.Api.Services;

public partial class SmsService
public partial class PhoneService
{
[AutoInject] private readonly AppSettings appSettings = default!;
[AutoInject] private readonly ILogger<SmsService> logger = default!;
[AutoInject] private readonly ILogger<PhoneService> logger = default!;
[AutoInject] private readonly IHostEnvironment hostEnvironment = default!;
[AutoInject] private readonly IHttpContextAccessor httpContextAccessor = default!;
[AutoInject] private readonly PhoneNumberUtil phoneNumberUtil = default!;
private const string APP_DEFAULT_REGION = "US" /*Two letter ISO region name*/;

public string? NormalizePhoneNumber(string? phoneNumber)
{
if (string.IsNullOrEmpty(phoneNumber))
return null;

// Get region from Cloudflare "CF-IPCountry" header if available, otherwise use UI culture's region if multilingual is enabled, or fallback to the default region.
var region = httpContextAccessor.HttpContext!.Request.Headers.TryGetValue("CF-IPCountry", out var value)
? value.ToString()
: CultureInfoManager.MultilingualEnabled
? new RegionInfo(CultureInfo.CurrentUICulture.Name).TwoLetterISORegionName
: APP_DEFAULT_REGION;

var parsedPhoneNumber = phoneNumberUtil.Parse(phoneNumber, region);

return phoneNumberUtil.Format(parsedPhoneNumber, PhoneNumberFormat.E164);
}

public async Task SendSms(string messageText, string phoneNumber, CancellationToken cancellationToken)
{
Expand Down

0 comments on commit c2d9728

Please sign in to comment.