Skip to content

Make IEmailSender more customizable #50301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Identity.UI.Services;

/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation emails.
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation and password reset emails.
/// </summary>
public interface IEmailSender
{
/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation emails.
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation and apassword reset emails.
/// </summary>
/// <param name="email">The recipient's email address.</param>
/// <param name="subject">The subject of the email.</param>
Expand Down
41 changes: 41 additions & 0 deletions src/Identity/Core/src/IEmailSenderOfT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Identity;

/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation and password reset emails.
/// </summary>
public interface IEmailSender<TUser> where TUser : class
{
/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send confirmation emails.
/// </summary>
/// <param name="user">The user that is attempting to confirm their email.</param>
/// <param name="email">The recipient's email address.</param>
/// <param name="confirmationLink">The link to follow to confirm a user's email. Do not double encode this.</param>
/// <returns></returns>
Task SendConfirmationLinkAsync(TUser user, string email, string confirmationLink);

/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send password reset emails.
/// </summary>
/// <param name="user">The user that is attempting to reset their password.</param>
/// <param name="email">The recipient's email address.</param>
/// <param name="resetLink">The link to follow to reset the user password. Do not double encode this.</param>
/// <returns></returns>
Task SendPasswordResetLinkAsync(TUser user, string email, string resetLink);

/// <summary>
/// This API supports the ASP.NET Core Identity infrastructure and is not intended to be used as a general purpose
/// email abstraction. It should be implemented by the application so the Identity infrastructure can send password reset emails.
/// </summary>
/// <param name="user">The user that is attempting to reset their password.</param>
/// <param name="email">The recipient's email address.</param>
/// <param name="resetCode">The code to use to reset the user password. Do not double encode this.</param>
/// <returns></returns>
Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -45,7 +44,7 @@ public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRou

var timeProvider = endpoints.ServiceProvider.GetRequiredService<TimeProvider>();
var bearerTokenOptions = endpoints.ServiceProvider.GetRequiredService<IOptionsMonitor<BearerTokenOptions>>();
var emailSender = endpoints.ServiceProvider.GetRequiredService<IEmailSender>();
var emailSender = endpoints.ServiceProvider.GetRequiredService<IEmailSender<TUser>>();
var linkGenerator = endpoints.ServiceProvider.GetRequiredService<LinkGenerator>();

// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
Expand Down Expand Up @@ -189,7 +188,6 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not T
var finalPattern = ((RouteEndpointBuilder)endpointBuilder).RoutePattern.RawText;
confirmEmailEndpointName = $"{nameof(MapIdentityApi)}-{finalPattern}";
endpointBuilder.Metadata.Add(new EndpointNameMetadata(confirmEmailEndpointName));
endpointBuilder.Metadata.Add(new RouteNameMetadata(confirmEmailEndpointName));
});

routeGroup.MapPost("/resendConfirmationEmail", async Task<Ok>
Expand All @@ -216,8 +214,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not T
var code = await userManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

await emailSender.SendEmailAsync(resetRequest.Email, "Reset your password",
$"Reset your password using the following code: {HtmlEncoder.Default.Encode(code)}");
await emailSender.SendPasswordResetCodeAsync(user, resetRequest.Email, HtmlEncoder.Default.Encode(code));
}

// Don't reveal that the user does not exist or is not confirmed, so don't return a 200 if we would have
Expand Down Expand Up @@ -416,8 +413,7 @@ async Task SendConfirmationEmailAsync(TUser user, UserManager<TUser> userManager
var confirmEmailUrl = linkGenerator.GetUriByName(context, confirmEmailEndpointName, routeValues)
?? throw new NotSupportedException($"Could not find endpoint named '{confirmEmailEndpointName}'.");

await emailSender.SendEmailAsync(email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(confirmEmailUrl)}'>clicking here</a>.");
await emailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(confirmEmailUrl));
}

return new IdentityEndpointsConventionBuilder(routeGroup);
Expand Down
1 change: 1 addition & 0 deletions src/Identity/Core/src/IdentityBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder)

builder.AddSignInManager();
builder.AddDefaultTokenProviders();
builder.Services.TryAddTransient(typeof(IEmailSender<>), typeof(DefaultMessageEmailSender<>));
builder.Services.TryAddTransient<IEmailSender, NoOpEmailSender>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, IdentityEndpointsJsonOptionsSetup>());
return builder;
Expand Down
2 changes: 1 addition & 1 deletion src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)BearerToken\DTO\*.cs" LinkBase="DTO" />
<Compile Include="$(SharedSourceRoot)DefaultMessageEmailSender.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Identity.UI.Services;

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Identity/Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,22 @@ Microsoft.AspNetCore.Identity.Data.TwoFactorResponse.RecoveryCodesLeft.init -> v
Microsoft.AspNetCore.Identity.Data.TwoFactorResponse.SharedKey.get -> string!
Microsoft.AspNetCore.Identity.Data.TwoFactorResponse.SharedKey.init -> void
Microsoft.AspNetCore.Identity.Data.TwoFactorResponse.TwoFactorResponse() -> void
Microsoft.AspNetCore.Identity.IEmailSender<TUser>
Microsoft.AspNetCore.Identity.IEmailSender<TUser>.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.IEmailSender<TUser>.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.IEmailSender<TUser>.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.SecurityStampValidator<TUser>.SecurityStampValidator(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions!>! options, Microsoft.AspNetCore.Identity.SignInManager<TUser!>! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void
Microsoft.AspNetCore.Identity.SecurityStampValidator<TUser>.TimeProvider.get -> System.TimeProvider!
Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.get -> System.TimeProvider?
Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.set -> void
Microsoft.AspNetCore.Identity.SignInManager<TUser>.AuthenticationScheme.get -> string!
Microsoft.AspNetCore.Identity.SignInManager<TUser>.AuthenticationScheme.set -> void
Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator<TUser>.TwoFactorSecurityStampValidator(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions!>! options, Microsoft.AspNetCore.Identity.SignInManager<TUser!>! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void
Microsoft.AspNetCore.Identity.UI.Services.IEmailSender
Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender.NoOpEmailSender() -> void
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions
static Microsoft.AspNetCore.Identity.IdentityBuilderExtensions.AddApiEndpoints(this Microsoft.AspNetCore.Identity.IdentityBuilder! builder) -> Microsoft.AspNetCore.Identity.IdentityBuilder!
static Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi<TUser>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
Expand Down
5 changes: 0 additions & 5 deletions src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
Microsoft.AspNetCore.Identity.IdentitySchemaVersions
Microsoft.AspNetCore.Identity.StoreOptions.SchemaVersion.get -> System.Version!
Microsoft.AspNetCore.Identity.StoreOptions.SchemaVersion.set -> void
Microsoft.AspNetCore.Identity.UI.Services.IEmailSender
Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender.NoOpEmailSender() -> void
Microsoft.AspNetCore.Identity.UI.Services.NoOpEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task!
static readonly Microsoft.AspNetCore.Identity.IdentitySchemaVersions.Default -> System.Version!
static readonly Microsoft.AspNetCore.Identity.IdentitySchemaVersions.Version1 -> System.Version!
static readonly Microsoft.AspNetCore.Identity.IdentitySchemaVersions.Version2 -> System.Version!
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
Expand Down Expand Up @@ -95,15 +94,15 @@ internal sealed class ExternalLoginModel<TUser> : ExternalLoginModel where TUser
private readonly UserManager<TUser> _userManager;
private readonly IUserStore<TUser> _userStore;
private readonly IUserEmailStore<TUser> _emailStore;
private readonly IEmailSender _emailSender;
private readonly IEmailSender<TUser> _emailSender;
private readonly ILogger<ExternalLoginModel> _logger;

public ExternalLoginModel(
SignInManager<TUser> signInManager,
UserManager<TUser> userManager,
IUserStore<TUser> userStore,
ILogger<ExternalLoginModel> logger,
IEmailSender emailSender)
IEmailSender<TUser> emailSender)
{
_signInManager = signInManager;
_userManager = userManager;
Expand Down Expand Up @@ -206,8 +205,7 @@ public override async Task<IActionResult> OnPostConfirmationAsync(string? return
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme)!;

await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

// If account confirmation is required, we need to show the link if we don't have a real email sender
if (_userManager.Options.SignIn.RequireConfirmedAccount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
Expand Down Expand Up @@ -52,9 +51,9 @@ public class InputModel
internal sealed class ForgotPasswordModel<TUser> : ForgotPasswordModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly IEmailSender _emailSender;
private readonly IEmailSender<TUser> _emailSender;

public ForgotPasswordModel(UserManager<TUser> userManager, IEmailSender emailSender)
public ForgotPasswordModel(UserManager<TUser> userManager, IEmailSender<TUser> emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
Expand All @@ -81,10 +80,7 @@ public override async Task<IActionResult> OnPostAsync()
values: new { area = "Identity", code },
protocol: Request.Scheme)!;

await _emailSender.SendEmailAsync(
Input.Email,
"Reset Password",
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _emailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

return RedirectToPage("./ForgotPasswordConfirmation");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
Expand Down Expand Up @@ -83,12 +82,12 @@ internal sealed class EmailModel<TUser> : EmailModel where TUser : class
{
private readonly UserManager<TUser> _userManager;
private readonly SignInManager<TUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly IEmailSender<TUser> _emailSender;

public EmailModel(
UserManager<TUser> userManager,
SignInManager<TUser> signInManager,
IEmailSender emailSender)
IEmailSender<TUser> emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
Expand Down Expand Up @@ -145,10 +144,7 @@ public override async Task<IActionResult> OnPostChangeEmailAsync()
pageHandler: null,
values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = code },
protocol: Request.Scheme)!;
await _emailSender.SendEmailAsync(
Input.NewEmail,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _emailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));

StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
Expand Down Expand Up @@ -181,10 +177,7 @@ public override async Task<IActionResult> OnPostSendVerificationEmailAsync()
pageHandler: null,
values: new { area = "Identity", userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email!,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl!)}'>clicking here</a>.");
await _emailSender.SendConfirmationLinkAsync(user, email!, HtmlEncoder.Default.Encode(callbackUrl!));

StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
Expand Down Expand Up @@ -98,14 +97,14 @@ internal sealed class RegisterModel<TUser> : RegisterModel where TUser : class
private readonly IUserStore<TUser> _userStore;
private readonly IUserEmailStore<TUser> _emailStore;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
private readonly IEmailSender<TUser> _emailSender;

public RegisterModel(
UserManager<TUser> userManager,
IUserStore<TUser> userStore,
SignInManager<TUser> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
IEmailSender<TUser> emailSender)
{
_userManager = userManager;
_userStore = userStore;
Expand Down Expand Up @@ -146,8 +145,7 @@ public override async Task<IActionResult> OnPostAsync(string? returnUrl = null)
values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
protocol: Request.Scheme)!;

await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
Expand Down
Loading