From 74eff8e3c1e74e63550856ee5b6082c3678eb095 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 23 Aug 2023 14:42:02 -0700 Subject: [PATCH 1/5] Make IEmailSender more customizable --- src/Identity/Core/src/IEmailSender.cs | 57 +++++++++++++++++++ ...entityApiEndpointRouteBuilderExtensions.cs | 6 +- .../src/NoOpEmailSender.cs | 2 - src/Identity/Core/src/PublicAPI.Unshipped.txt | 8 +++ .../Extensions.Core/src/IEmailSender.cs | 23 -------- .../src/PublicAPI.Unshipped.txt | 5 -- .../Pages/V4/Account/ExternalLogin.cshtml.cs | 3 +- .../Pages/V4/Account/ForgotPassword.cshtml.cs | 5 +- .../Pages/V4/Account/Manage/Email.cshtml.cs | 10 +--- .../Pages/V4/Account/Register.cshtml.cs | 3 +- .../Account/ResendEmailConfirmation.cshtml.cs | 5 +- .../Pages/V5/Account/ExternalLogin.cshtml.cs | 3 +- .../Pages/V5/Account/ForgotPassword.cshtml.cs | 5 +- .../Pages/V5/Account/Manage/Email.cshtml.cs | 10 +--- .../Pages/V5/Account/Register.cshtml.cs | 3 +- .../Account/ResendEmailConfirmation.cshtml.cs | 5 +- src/Identity/UI/src/PublicAPI.Unshipped.txt | 7 ++- .../Identity/Pages/Account/Register.cshtml.cs | 3 +- 18 files changed, 85 insertions(+), 78 deletions(-) create mode 100644 src/Identity/Core/src/IEmailSender.cs rename src/Identity/{Extensions.Core => Core}/src/NoOpEmailSender.cs (96%) delete mode 100644 src/Identity/Extensions.Core/src/IEmailSender.cs diff --git a/src/Identity/Core/src/IEmailSender.cs b/src/Identity/Core/src/IEmailSender.cs new file mode 100644 index 000000000000..e666ec7c2883 --- /dev/null +++ b/src/Identity/Core/src/IEmailSender.cs @@ -0,0 +1,57 @@ +// 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.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 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 and apassword reset emails. + /// + /// The recipient's email address. + /// The subject of the email. + /// The body of the email which may contain HTML tags. Do not double encode this. + /// + Task SendEmailAsync(string email, string subject, string htmlMessage); + + /// + /// 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 recipient's email address. + /// The link to follow to confirm a user's email. Do not double encode this. + /// + Task SendConfirmationLinkAsync(string email, string confirmationLink) + { + return SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); + } + + /// + /// 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 recipient's email address. + /// The link to follow to reset the user password. Do not double encode this. + /// + Task SendPasswordResetLinkAsync(string email, string resetLink) + { + return SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); + } + + /// + /// 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 recipient's email address. + /// The code to use to reset the user password. Do not double encode this. + /// + Task SendPasswordResetCodeAsync(string email, string resetCode) + { + return SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); + } +} diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index 00a7022ae43c..40caa52b96af 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -216,8 +216,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(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 +415,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(email, HtmlEncoder.Default.Encode(confirmEmailUrl)); } return new IdentityEndpointsConventionBuilder(routeGroup); 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..0ae9601c43de 100644 --- a/src/Identity/Core/src/PublicAPI.Unshipped.txt +++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt @@ -82,6 +82,14 @@ 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.SendConfirmationLinkAsync(string! email, string! confirmationLink) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(string! email, string! resetCode) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(string! email, string! resetLink) -> 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/IEmailSender.cs b/src/Identity/Extensions.Core/src/IEmailSender.cs deleted file mode 100644 index 614a1fd6254e..000000000000 --- a/src/Identity/Extensions.Core/src/IEmailSender.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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. -/// -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. - /// - /// The recipient's email address. - /// The subject of the email. - /// The body of the email which may contain HTML tags. Do not double encode this. - /// - Task SendEmailAsync(string email, string subject, string htmlMessage); -} 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..7d6572951e05 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 @@ -206,8 +206,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(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..53a622a32079 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 @@ -81,10 +81,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(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..4a55fc119b19 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 @@ -145,10 +145,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(Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); StatusMessage = "Confirmation link to change email sent. Please check your email."; return RedirectToPage(); @@ -181,10 +178,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(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..afe0db16d7b3 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 @@ -146,8 +146,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(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); if (_userManager.Options.SignIn.RequireConfirmedAccount) { 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..90b7910b31de 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 @@ -92,10 +92,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(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..448eacb4be9a 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 @@ -206,8 +206,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(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..424145a72b62 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 @@ -81,10 +81,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(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..257bc0e9b83e 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 @@ -145,10 +145,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(Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); StatusMessage = "Confirmation link to change email sent. Please check your email."; return RedirectToPage(); @@ -181,10 +178,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(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..3c75614ab162 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 @@ -146,8 +146,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(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); if (_userManager.Options.SignIn.RequireConfirmedAccount) { 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..daceaa1e3798 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 @@ -92,10 +92,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(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/PublicAPI.Unshipped.txt b/src/Identity/UI/src/PublicAPI.Unshipped.txt index 4a3b9f9670bb..91c9c208ee0b 100644 --- a/src/Identity/UI/src/PublicAPI.Unshipped.txt +++ b/src/Identity/UI/src/PublicAPI.Unshipped.txt @@ -1,5 +1,8 @@ #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) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendConfirmationLinkAsync(string! email, string! confirmationLink) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(string! email, string! resetLink) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(string! email, string! resetCode) -> 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..768aabcbebc3 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 @@ -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(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); if (_userManager.Options.SignIn.RequireConfirmedAccount) { From 8933d1e03e1f04091465b2b2d2246200a63891f3 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 23 Aug 2023 15:27:02 -0700 Subject: [PATCH 2/5] Remove unnecessary metadata --- .../Core/src/IdentityApiEndpointRouteBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index 40caa52b96af..cb8ed4d8f15b 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -189,7 +189,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 From 439fd5a4fa03d704dc71f3d9fecca6c6472bac21 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 23 Aug 2023 16:41:59 -0700 Subject: [PATCH 3/5] Add TUser parameter --- src/Identity/Core/src/IEmailSender.cs | 13 ++++++++----- .../IdentityApiEndpointRouteBuilderExtensions.cs | 4 ++-- src/Identity/Core/src/PublicAPI.Unshipped.txt | 6 +++--- .../Pages/V4/Account/ExternalLogin.cshtml.cs | 2 +- .../Pages/V4/Account/ForgotPassword.cshtml.cs | 2 +- .../Pages/V4/Account/Manage/Email.cshtml.cs | 4 ++-- .../Identity/Pages/V4/Account/Register.cshtml.cs | 2 +- .../V4/Account/ResendEmailConfirmation.cshtml.cs | 2 +- .../Pages/V5/Account/ExternalLogin.cshtml.cs | 2 +- .../Pages/V5/Account/ForgotPassword.cshtml.cs | 2 +- .../Pages/V5/Account/Manage/Email.cshtml.cs | 4 ++-- .../Identity/Pages/V5/Account/Register.cshtml.cs | 2 +- .../V5/Account/ResendEmailConfirmation.cshtml.cs | 2 +- src/Identity/UI/src/PublicAPI.Unshipped.txt | 6 +++--- .../Areas/Identity/Pages/Account/Register.cshtml.cs | 2 +- 15 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/Identity/Core/src/IEmailSender.cs b/src/Identity/Core/src/IEmailSender.cs index e666ec7c2883..ccf3a9580487 100644 --- a/src/Identity/Core/src/IEmailSender.cs +++ b/src/Identity/Core/src/IEmailSender.cs @@ -23,34 +23,37 @@ 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. /// + /// 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(string email, string confirmationLink) + Task SendConfirmationLinkAsync(TUser user, string email, string confirmationLink) where TUser : class { return SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); } /// /// 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 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(string email, string resetLink) + Task SendPasswordResetLinkAsync(TUser user, string email, string resetLink) where TUser : class { return SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); } /// /// 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 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(string email, string resetCode) + Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode) where TUser : class { return SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); } diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index cb8ed4d8f15b..8f8ae43b3142 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -215,7 +215,7 @@ await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not T var code = await userManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); - await emailSender.SendPasswordResetCodeAsync(resetRequest.Email, 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 @@ -414,7 +414,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.SendConfirmationLinkAsync(email, HtmlEncoder.Default.Encode(confirmEmailUrl)); + await emailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(confirmEmailUrl)); } return new IdentityEndpointsConventionBuilder(routeGroup); diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt index 0ae9601c43de..bc56f49ba630 100644 --- a/src/Identity/Core/src/PublicAPI.Unshipped.txt +++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt @@ -83,10 +83,10 @@ Microsoft.AspNetCore.Identity.SignInManager.AuthenticationScheme.get -> s 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.SendConfirmationLinkAsync(string! email, string! confirmationLink) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(string! email, string! resetCode) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(string! email, string! resetLink) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> 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! 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 7d6572951e05..f5fd64000cad 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 @@ -206,7 +206,7 @@ public override async Task OnPostConfirmationAsync(string? return values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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 53a622a32079..6d465c499c96 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 @@ -81,7 +81,7 @@ public override async Task OnPostAsync() values: new { area = "Identity", code }, protocol: Request.Scheme)!; - await _emailSender.SendPasswordResetLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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 4a55fc119b19..a1b72ebddb63 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 @@ -145,7 +145,7 @@ public override async Task OnPostChangeEmailAsync() pageHandler: null, values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); + await _emailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); StatusMessage = "Confirmation link to change email sent. Please check your email."; return RedirectToPage(); @@ -178,7 +178,7 @@ public override async Task OnPostSendVerificationEmailAsync() pageHandler: null, values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme); - await _emailSender.SendConfirmationLinkAsync(email!, HtmlEncoder.Default.Encode(callbackUrl!)); + 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 afe0db16d7b3..ed103e9807f0 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 @@ -146,7 +146,7 @@ public override async Task OnPostAsync(string? returnUrl = null) values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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/ResendEmailConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/ResendEmailConfirmation.cshtml.cs index 90b7910b31de..403f404bd6e1 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 @@ -92,7 +92,7 @@ public override async Task OnPostAsync() pageHandler: null, values: new { userId = userId, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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 448eacb4be9a..fada28d0d340 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 @@ -206,7 +206,7 @@ public override async Task OnPostConfirmationAsync(string? return values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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 424145a72b62..bcb753dc5ecf 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 @@ -81,7 +81,7 @@ public override async Task OnPostAsync() values: new { area = "Identity", code }, protocol: Request.Scheme)!; - await _emailSender.SendPasswordResetLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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 257bc0e9b83e..25c3dc083d30 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 @@ -145,7 +145,7 @@ public override async Task OnPostChangeEmailAsync() pageHandler: null, values: new { area = "Identity", userId = userId, email = Input.NewEmail, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); + await _emailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); StatusMessage = "Confirmation link to change email sent. Please check your email."; return RedirectToPage(); @@ -178,7 +178,7 @@ public override async Task OnPostSendVerificationEmailAsync() pageHandler: null, values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(email!, HtmlEncoder.Default.Encode(callbackUrl)); + 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 3c75614ab162..9f3f609a14cf 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 @@ -146,7 +146,7 @@ public override async Task OnPostAsync(string? returnUrl = null) values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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/ResendEmailConfirmation.cshtml.cs b/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/ResendEmailConfirmation.cshtml.cs index daceaa1e3798..3445294eecda 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 @@ -92,7 +92,7 @@ public override async Task OnPostAsync() pageHandler: null, values: new { userId = userId, code = code }, protocol: Request.Scheme)!; - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + 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/PublicAPI.Unshipped.txt b/src/Identity/UI/src/PublicAPI.Unshipped.txt index 91c9c208ee0b..b89a25d76275 100644 --- a/src/Identity/UI/src/PublicAPI.Unshipped.txt +++ b/src/Identity/UI/src/PublicAPI.Unshipped.txt @@ -2,7 +2,7 @@ *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.AspNetCore.Identity) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task! (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) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendConfirmationLinkAsync(string! email, string! confirmationLink) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(string! email, string! resetLink) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(string! email, string! resetCode) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) +Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> 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 768aabcbebc3..702c506101d7 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 @@ -99,7 +99,7 @@ public async Task OnPostAsync(string returnUrl = null) values: new { userId = user.Id, code = code }, protocol: Request.Scheme); - await _emailSender.SendConfirmationLinkAsync(Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + await _emailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); if (_userManager.Options.SignIn.RequireConfirmedAccount) { From 118a71797e10fa28bd4d65cc7afe0bf9744f311b Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 18 Sep 2023 14:09:14 -0700 Subject: [PATCH 4/5] React to API review feedback --- src/Identity/Core/src/IEmailSender.cs | 39 ------------------ src/Identity/Core/src/IEmailSenderOfT.cs | 41 +++++++++++++++++++ ...entityApiEndpointRouteBuilderExtensions.cs | 3 +- .../Core/src/IdentityBuilderExtensions.cs | 1 + .../src/Microsoft.AspNetCore.Identity.csproj | 2 +- src/Identity/Core/src/PublicAPI.Unshipped.txt | 7 ++-- .../Pages/V4/Account/ExternalLogin.cshtml.cs | 5 +-- .../Pages/V4/Account/ForgotPassword.cshtml.cs | 5 +-- .../Pages/V4/Account/Manage/Email.cshtml.cs | 5 +-- .../Pages/V4/Account/Register.cshtml.cs | 5 +-- .../V4/Account/RegisterConfirmation.cshtml.cs | 7 ++-- .../Account/ResendEmailConfirmation.cshtml.cs | 5 +-- .../Pages/V5/Account/ExternalLogin.cshtml.cs | 5 +-- .../Pages/V5/Account/ForgotPassword.cshtml.cs | 5 +-- .../Pages/V5/Account/Manage/Email.cshtml.cs | 5 +-- .../Pages/V5/Account/Register.cshtml.cs | 5 +-- .../V5/Account/RegisterConfirmation.cshtml.cs | 7 ++-- .../Account/ResendEmailConfirmation.cshtml.cs | 5 +-- .../UI/src/IdentityBuilderUIExtensions.cs | 1 + .../Microsoft.AspNetCore.Identity.UI.csproj | 4 ++ src/Identity/UI/src/PublicAPI.Unshipped.txt | 3 -- .../MapIdentityApiTests.cs | 40 ++++++++++++++++++ src/Shared/DefaultMessageEmailSender.cs | 20 +++++++++ 23 files changed, 139 insertions(+), 86 deletions(-) create mode 100644 src/Identity/Core/src/IEmailSenderOfT.cs create mode 100644 src/Shared/DefaultMessageEmailSender.cs diff --git a/src/Identity/Core/src/IEmailSender.cs b/src/Identity/Core/src/IEmailSender.cs index ccf3a9580487..18e2959c9b39 100644 --- a/src/Identity/Core/src/IEmailSender.cs +++ b/src/Identity/Core/src/IEmailSender.cs @@ -18,43 +18,4 @@ public interface IEmailSender /// The body of the email which may contain HTML tags. Do not double encode this. /// Task SendEmailAsync(string email, string subject, string htmlMessage); - - /// - /// 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) where TUser : class - { - return SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); - } - - /// - /// 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) where TUser : class - { - return SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); - } - - /// - /// 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) where TUser : class - { - return SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); - } } 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 8f8ae43b3142..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. 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/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt index bc56f49ba630..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? @@ -83,10 +87,7 @@ Microsoft.AspNetCore.Identity.SignInManager.AuthenticationScheme.get -> s 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.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendEmailAsync(string! email, string! subject, string! htmlMessage) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> 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! 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 f5fd64000cad..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; 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 6d465c499c96..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; 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 a1b72ebddb63..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; 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 ed103e9807f0..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; 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 403f404bd6e1..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; 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 fada28d0d340..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; 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 bcb753dc5ecf..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; 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 25c3dc083d30..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; 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 9f3f609a14cf..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; 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 3445294eecda..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; 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 b89a25d76275..cbdf5a233663 100644 --- a/src/Identity/UI/src/PublicAPI.Unshipped.txt +++ b/src/Identity/UI/src/PublicAPI.Unshipped.txt @@ -2,7 +2,4 @@ *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.AspNetCore.Identity) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendConfirmationLinkAsync(TUser! user, string! email, string! confirmationLink) -> System.Threading.Tasks.Task! (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) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetCodeAsync(TUser! user, string! email, string! resetCode) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) -Microsoft.AspNetCore.Identity.UI.Services.IEmailSender.SendPasswordResetLinkAsync(TUser! user, string! email, string! resetLink) -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Identity) 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}"); +} From e0b7ecb971a08c9426e02ae4e96ef08384f89136 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 18 Sep 2023 14:49:32 -0700 Subject: [PATCH 5/5] Fix IdentitySample.DefaultUI --- .../Areas/Identity/Pages/Account/Register.cshtml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 702c506101d7..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;