diff --git a/src/Identity/Extensions.Core/src/IEmailSender.cs b/src/Identity/Core/src/IEmailSender.cs
similarity index 85%
rename from src/Identity/Extensions.Core/src/IEmailSender.cs
rename to src/Identity/Core/src/IEmailSender.cs
index 614a1fd6254e..18e2959c9b39 100644
--- a/src/Identity/Extensions.Core/src/IEmailSender.cs
+++ b/src/Identity/Core/src/IEmailSender.cs
@@ -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;
///
/// 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.
///
public interface IEmailSender
{
///
/// 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.
///
/// The recipient's email address.
/// The subject of the email.
diff --git a/src/Identity/Core/src/IEmailSenderOfT.cs b/src/Identity/Core/src/IEmailSenderOfT.cs
new file mode 100644
index 000000000000..d83735db7e6d
--- /dev/null
+++ b/src/Identity/Core/src/IEmailSenderOfT.cs
@@ -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;
+
+///
+/// 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.
+///
+public interface IEmailSender where TUser : class
+{
+ ///
+ /// 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.
+ ///
+ /// The user that is attempting to confirm their email.
+ /// The recipient's email address.
+ /// The link to follow to confirm a user's email. Do not double encode this.
+ ///
+ Task SendConfirmationLinkAsync(TUser user, string email, string confirmationLink);
+
+ ///
+ /// 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.
+ ///
+ /// The user that is attempting to reset their password.
+ /// The recipient's email address.
+ /// The link to follow to reset the user password. Do not double encode this.
+ ///
+ Task SendPasswordResetLinkAsync(TUser user, string email, string resetLink);
+
+ ///
+ /// 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.
+ ///
+ /// The user that is attempting to reset their password.
+ /// The recipient's email address.
+ /// The code to use to reset the user password. Do not double encode this.
+ ///
+ Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode);
+}
diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
index 00a7022ae43c..8a9ed5ae1431 100644
--- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
+++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
@@ -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;
@@ -45,7 +44,7 @@ public static IEndpointConventionBuilder MapIdentityApi(this IEndpointRou
var timeProvider = endpoints.ServiceProvider.GetRequiredService();
var bearerTokenOptions = endpoints.ServiceProvider.GetRequiredService>();
- var emailSender = endpoints.ServiceProvider.GetRequiredService();
+ var emailSender = endpoints.ServiceProvider.GetRequiredService>();
var linkGenerator = endpoints.ServiceProvider.GetRequiredService();
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
@@ -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
@@ -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
@@ -416,8 +413,7 @@ async Task SendConfirmationEmailAsync(TUser user, UserManager 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 clicking here.");
+ await emailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(confirmEmailUrl));
}
return new IdentityEndpointsConventionBuilder(routeGroup);
diff --git a/src/Identity/Core/src/IdentityBuilderExtensions.cs b/src/Identity/Core/src/IdentityBuilderExtensions.cs
index e51c6ee31ef2..eec4e9d04ede 100644
--- a/src/Identity/Core/src/IdentityBuilderExtensions.cs
+++ b/src/Identity/Core/src/IdentityBuilderExtensions.cs
@@ -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();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, IdentityEndpointsJsonOptionsSetup>());
return builder;
diff --git a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
index 1488733572e1..991c08e76922 100644
--- a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
+++ b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/Identity/Extensions.Core/src/NoOpEmailSender.cs b/src/Identity/Core/src/NoOpEmailSender.cs
similarity index 96%
rename from src/Identity/Extensions.Core/src/NoOpEmailSender.cs
rename to src/Identity/Core/src/NoOpEmailSender.cs
index aadc3dd502a6..6ad0b62ea648 100644
--- a/src/Identity/Extensions.Core/src/NoOpEmailSender.cs
+++ b/src/Identity/Core/src/NoOpEmailSender.cs
@@ -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;
///
diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt
index 2807929dcfd1..8b9f4af02588 100644
--- a/src/Identity/Core/src/PublicAPI.Unshipped.txt
+++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt
@@ -75,6 +75,10 @@ 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
+Microsoft.AspNetCore.Identity.IEmailSender.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.IEmailSender.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.IEmailSender.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.SecurityStampValidator.SecurityStampValidator(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Identity.SignInManager! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void
Microsoft.AspNetCore.Identity.SecurityStampValidator.TimeProvider.get -> System.TimeProvider!
Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.get -> System.TimeProvider?
@@ -82,6 +86,11 @@ Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.set ->
Microsoft.AspNetCore.Identity.SignInManager.AuthenticationScheme.get -> string!
Microsoft.AspNetCore.Identity.SignInManager.AuthenticationScheme.set -> void
Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator.TwoFactorSecurityStampValidator(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Identity.SignInManager! 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(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
diff --git a/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt b/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
index 33bb8e55c6e3..d9173be85cd0 100644
--- a/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
+++ b/src/Identity/Extensions.Core/src/PublicAPI.Unshipped.txt
@@ -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!
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs
index c7ee2f10179f..30eac04d72c8 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ExternalLogin.cshtml.cs
@@ -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;
@@ -95,7 +94,7 @@ internal sealed class ExternalLoginModel : ExternalLoginModel where TUser
private readonly UserManager _userManager;
private readonly IUserStore _userStore;
private readonly IUserEmailStore _emailStore;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
public ExternalLoginModel(
@@ -103,7 +102,7 @@ public ExternalLoginModel(
UserManager userManager,
IUserStore userStore,
ILogger logger,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_signInManager = signInManager;
_userManager = userManager;
@@ -206,8 +205,7 @@ public override async Task 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 clicking here.");
+ 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)
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml.cs
index 2a30467aa485..9ef8778f352a 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ForgotPassword.cshtml.cs
@@ -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;
@@ -52,9 +51,9 @@ public class InputModel
internal sealed class ForgotPasswordModel : ForgotPasswordModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
- public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender)
+ public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
@@ -81,10 +80,7 @@ public override async Task OnPostAsync()
values: new { area = "Identity", code },
protocol: Request.Scheme)!;
- await _emailSender.SendEmailAsync(
- Input.Email,
- "Reset Password",
- $"Please reset your password by clicking here.");
+ await _emailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
return RedirectToPage("./ForgotPasswordConfirmation");
}
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Email.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Email.cshtml.cs
index d061d219c465..163feb1f00ba 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Email.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/Email.cshtml.cs
@@ -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;
@@ -83,12 +82,12 @@ internal sealed class EmailModel : EmailModel where TUser : class
{
private readonly UserManager _userManager;
private readonly SignInManager _signInManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
public EmailModel(
UserManager userManager,
SignInManager signInManager,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -145,10 +144,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
@@ -181,10 +177,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, email!, HtmlEncoder.Default.Encode(callbackUrl!));
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs
index c783a8be7927..c8390ae7c62f 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml.cs
@@ -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;
@@ -98,14 +97,14 @@ internal sealed class RegisterModel : RegisterModel where TUser : class
private readonly IUserStore _userStore;
private readonly IUserEmailStore _emailStore;
private readonly ILogger _logger;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager userManager,
IUserStore userStore,
SignInManager signInManager,
ILogger logger,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_userManager = userManager;
_userStore = userStore;
@@ -146,8 +145,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs
index 0423e2820fee..4af237c896e2 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs
@@ -3,7 +3,6 @@
using System.Text;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
@@ -46,9 +45,9 @@ public class RegisterConfirmationModel : PageModel
internal sealed class RegisterConfirmationModel : RegisterConfirmationModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _sender;
+ private readonly IEmailSender _sender;
- public RegisterConfirmationModel(UserManager userManager, IEmailSender sender)
+ public RegisterConfirmationModel(UserManager userManager, IEmailSender sender)
{
_userManager = userManager;
_sender = sender;
@@ -70,7 +69,7 @@ public override async Task OnGetAsync(string email, string? retur
Email = email;
// If the email sender is a no-op, display the confirm link in the page
- DisplayConfirmAccountLink = _sender is NoOpEmailSender;
+ DisplayConfirmAccountLink = _sender is DefaultMessageEmailSender defaultMessageSender && defaultMessageSender.IsNoOp;
if (DisplayConfirmAccountLink)
{
var userId = await _userManager.GetUserIdAsync(user);
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResendEmailConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResendEmailConfirmation.cshtml.cs
index f6e3893fbcb4..c0d18466ceae 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResendEmailConfirmation.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResendEmailConfirmation.cshtml.cs
@@ -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;
@@ -58,9 +57,9 @@ public class InputModel
internal sealed class ResendEmailConfirmationModel : ResendEmailConfirmationModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
- public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender)
+ public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
@@ -92,10 +91,7 @@ public override async Task OnPostAsync()
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme)!;
- await _emailSender.SendEmailAsync(
- Input.Email,
- "Confirm your email",
- $"Please confirm your account by clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
return Page();
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs
index 3187a4756a69..0eccb938ec26 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ExternalLogin.cshtml.cs
@@ -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;
@@ -95,7 +94,7 @@ internal sealed class ExternalLoginModel : ExternalLoginModel where TUser
private readonly UserManager _userManager;
private readonly IUserStore _userStore;
private readonly IUserEmailStore _emailStore;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
public ExternalLoginModel(
@@ -103,7 +102,7 @@ public ExternalLoginModel(
UserManager userManager,
IUserStore userStore,
ILogger logger,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_signInManager = signInManager;
_userManager = userManager;
@@ -206,8 +205,7 @@ public override async Task 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 clicking here.");
+ 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)
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ForgotPassword.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ForgotPassword.cshtml.cs
index ec8dc6fed965..6fea58f32175 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ForgotPassword.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ForgotPassword.cshtml.cs
@@ -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;
@@ -52,9 +51,9 @@ public class InputModel
internal sealed class ForgotPasswordModel : ForgotPasswordModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
- public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender)
+ public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
@@ -81,10 +80,7 @@ public override async Task OnPostAsync()
values: new { area = "Identity", code },
protocol: Request.Scheme)!;
- await _emailSender.SendEmailAsync(
- Input.Email,
- "Reset Password",
- $"Please reset your password by clicking here.");
+ await _emailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
return RedirectToPage("./ForgotPasswordConfirmation");
}
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Manage/Email.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Manage/Email.cshtml.cs
index c56438d0efe3..54bd9655fd96 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Manage/Email.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Manage/Email.cshtml.cs
@@ -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;
@@ -83,12 +82,12 @@ internal sealed class EmailModel : EmailModel where TUser : class
{
private readonly UserManager _userManager;
private readonly SignInManager _signInManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
public EmailModel(
UserManager userManager,
SignInManager signInManager,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -145,10 +144,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
StatusMessage = "Confirmation link to change email sent. Please check your email.";
return RedirectToPage();
@@ -181,10 +177,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, email!, HtmlEncoder.Default.Encode(callbackUrl));
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs
index daa94280f2d7..b55c9eba5167 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml.cs
@@ -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;
@@ -98,14 +97,14 @@ internal sealed class RegisterModel : RegisterModel where TUser : class
private readonly IUserStore _userStore;
private readonly IUserEmailStore _emailStore;
private readonly ILogger _logger;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager userManager,
IUserStore userStore,
SignInManager signInManager,
ILogger logger,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_userManager = userManager;
_userStore = userStore;
@@ -146,8 +145,7 @@ public override async Task 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 clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/RegisterConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/RegisterConfirmation.cshtml.cs
index 51a090db6f82..3003bb7f8d6e 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/RegisterConfirmation.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/RegisterConfirmation.cshtml.cs
@@ -3,7 +3,6 @@
using System.Text;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;
@@ -46,9 +45,9 @@ public class RegisterConfirmationModel : PageModel
internal sealed class RegisterConfirmationModel : RegisterConfirmationModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _sender;
+ private readonly IEmailSender _sender;
- public RegisterConfirmationModel(UserManager userManager, IEmailSender sender)
+ public RegisterConfirmationModel(UserManager userManager, IEmailSender sender)
{
_userManager = userManager;
_sender = sender;
@@ -70,7 +69,7 @@ public override async Task OnGetAsync(string email, string? retur
Email = email;
// If the email sender is a no-op, display the confirm link in the page
- DisplayConfirmAccountLink = _sender is NoOpEmailSender;
+ DisplayConfirmAccountLink = _sender is DefaultMessageEmailSender defaultMessageSender && defaultMessageSender.IsNoOp;
if (DisplayConfirmAccountLink)
{
var userId = await _userManager.GetUserIdAsync(user);
diff --git a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ResendEmailConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ResendEmailConfirmation.cshtml.cs
index b31525eb41e5..ba88ab597c83 100644
--- a/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ResendEmailConfirmation.cshtml.cs
+++ b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ResendEmailConfirmation.cshtml.cs
@@ -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;
@@ -58,9 +57,9 @@ public class InputModel
internal sealed class ResendEmailConfirmationModel : ResendEmailConfirmationModel where TUser : class
{
private readonly UserManager _userManager;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
- public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender)
+ public ResendEmailConfirmationModel(UserManager userManager, IEmailSender emailSender)
{
_userManager = userManager;
_emailSender = emailSender;
@@ -92,10 +91,7 @@ public override async Task OnPostAsync()
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme)!;
- await _emailSender.SendEmailAsync(
- Input.Email,
- "Confirm your email",
- $"Please confirm your account by clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email.");
return Page();
diff --git a/src/Identity/UI/src/IdentityBuilderUIExtensions.cs b/src/Identity/UI/src/IdentityBuilderUIExtensions.cs
index 014936cbbabe..ff7adbc2203d 100644
--- a/src/Identity/UI/src/IdentityBuilderUIExtensions.cs
+++ b/src/Identity/UI/src/IdentityBuilderUIExtensions.cs
@@ -60,6 +60,7 @@ public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
typeof(IdentityDefaultUIConfigureOptions<>)
.MakeGenericType(builder.UserType));
builder.Services.TryAddTransient();
+ builder.Services.TryAddTransient(typeof(IEmailSender<>), typeof(DefaultMessageEmailSender<>));
return builder;
}
diff --git a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
index 986c84729a81..135ff8d97735 100644
--- a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
+++ b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
@@ -20,6 +20,10 @@
GetIdentityUIAssets
+
+
+
+
diff --git a/src/Identity/UI/src/PublicAPI.Unshipped.txt b/src/Identity/UI/src/PublicAPI.Unshipped.txt
index 4a3b9f9670bb..cbdf5a233663 100644
--- a/src/Identity/UI/src/PublicAPI.Unshipped.txt
+++ b/src/Identity/UI/src/PublicAPI.Unshipped.txt
@@ -1,5 +1,5 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Identity.UI.Services.IEmailSender
*REMOVED*Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task!
-Microsoft.AspNetCore.Identity.UI.Services.IEmailSender (forwarded, contained in Microsoft.Extensions.Identity.Core)
-Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.Extensions.Identity.Core)
+Microsoft.AspNetCore.Identity.UI.Services.IEmailSender (forwarded, contained in Microsoft.AspNetCore.Identity)
+Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity)
diff --git a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml.cs b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml.cs
index 8e0cb0599a27..4a9806e417ef 100644
--- a/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml.cs
+++ b/src/Identity/samples/IdentitySample.DefaultUI/Areas/Identity/Pages/Account/Register.cshtml.cs
@@ -17,13 +17,13 @@ public class RegisterModel : PageModel
private readonly SignInManager _signInManager;
private readonly UserManager _userManager;
private readonly ILogger _logger;
- private readonly IEmailSender _emailSender;
+ private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager userManager,
SignInManager signInManager,
ILogger logger,
- IEmailSender emailSender)
+ IEmailSender emailSender)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -99,8 +99,7 @@ public async Task OnPostAsync(string returnUrl = null)
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
- await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
- $"Please confirm your account by clicking here.");
+ await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs
index bf74b839ba32..57ac19d7fccf 100644
--- a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs
+++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs
@@ -562,6 +562,27 @@ public async Task EmailConfirmationCanBeResent()
AssertOk(await client.PostAsJsonAsync("/identity/login", new { Email, Password }));
}
+ [Fact]
+ public async Task AccountConfirmationEmailCanBeCustomized()
+ {
+ var emailSender = new TestEmailSender();
+ var customEmailSender = new TestCustomEmailSender(emailSender);
+
+ await using var app = await CreateAppAsync(services =>
+ {
+ AddIdentityApiEndpoints(services);
+ services.AddSingleton>(customEmailSender);
+ });
+ using var client = app.GetTestClient();
+
+ await RegisterAsync(client);
+
+ var email = Assert.Single(emailSender.Emails);
+ Assert.Equal(Email, email.Address);
+ Assert.Equal(TestCustomEmailSender.CustomSubject, email.Subject);
+ Assert.Equal(TestCustomEmailSender.CustomMessage, email.HtmlMessage);
+ }
+
[Fact]
public async Task CanAddEndpointsToMultipleRouteGroupsForSameUserType()
{
@@ -1509,5 +1530,24 @@ public Task SendEmailAsync(string email, string subject, string htmlMessage)
}
}
+ private sealed class TestCustomEmailSender(IEmailSender emailSender) : IEmailSender
+ {
+ public const string CustomSubject = "Custom subject";
+ public const string CustomMessage = "Custom message";
+
+ public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink)
+ {
+ Assert.Equal(user.Email, email);
+ emailSender.SendEmailAsync(email, "Custom subject", "Custom message");
+ return Task.CompletedTask;
+ }
+
+ public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
+ throw new NotImplementedException();
+
+ public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
+ throw new NotImplementedException();
+ }
+
private sealed record TestEmail(string Address, string Subject, string HtmlMessage);
}
diff --git a/src/Shared/DefaultMessageEmailSender.cs b/src/Shared/DefaultMessageEmailSender.cs
new file mode 100644
index 000000000000..9c19065ceaf2
--- /dev/null
+++ b/src/Shared/DefaultMessageEmailSender.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Identity.UI.Services;
+
+namespace Microsoft.AspNetCore.Identity;
+
+internal sealed class DefaultMessageEmailSender(IEmailSender emailSender) : IEmailSender where TUser : class
+{
+ internal bool IsNoOp => emailSender is NoOpEmailSender;
+
+ public Task SendConfirmationLinkAsync(TUser user, string email, string confirmationLink) =>
+ emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here.");
+
+ public Task SendPasswordResetLinkAsync(TUser user, string email, string resetLink) =>
+ emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here.");
+
+ public Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode) =>
+ emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
+}