Skip to content

Make IEmailSender more easily customizable #50298

@halter73

Description

@halter73

Background and Motivation

Should this text be configurable eventually?

I'm reminded of the "We will never ask you for this code" story we heard recently 😄

Probably. It's not configurable with Identity UI, but you can more easily override razor pages than these endpoints. And you can scaffold them out. You could parse out the code in your IEmailSender and do whatever, but that'd obviously be very fragile.

I think in the future we might have a higher-level abstraction with methods like SendEmailConfirmationAsync and SendPasswordResetAsync and a TUser parameter. The default implementation could then just resolve the IEmailSender and use the existing strings. That's not something we need to do immediately though.

Originally posted by @halter73 in #49981 (comment)

We now also have a customer requesting this functionality in the preview7 blogpost at https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-7/comment-page-2/#comment-18682

Proposed API

- // Microsoft.Extensions.Identity.Core.dll
+ // Microsoft.AspNetCore.Identity.dll (the move to the shared framework only assembly allows DIMs

namespace Microsoft.AspNetCore.Identity.UI.Services;

public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string htmlMessage);

       // Used by MapIdentityApi and Identity UI
+    Task SendConfirmationLinkAsync<TUser>(TUser user, string email, string confirmationLink) where TUser : class
+    {
+        return SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
+    }

       // Used by Identity UI
+    Task SendPasswordResetLinkAsync<TUser>(TUser user, string email, string resetLink) where TUser : class
+    {
+        return SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
+    }

       // Used by MapIdentityApi since it does not provide a UI to enter the new password into. That's left to custom application code.
+    Task SendPasswordResetCodeAsync<TUser>(TUser user, string email, string resetCode) where TUser : class
+    {
+        return SendEmailAsync(email, "Reset your password", $"Reset your password using the following code: {resetCode}");
+    }
}

// Unchanged, but this also moves from Microsoft.Extensions.Identity.Core.dll to Microsoft.AspNetCore.Identity.dll,
// so it can still implement IEmailSender
public sealed class NoOpEmailSender : IEmailSender
{
    public Task SendEmailAsync(string email, string subject, string htmlMessage) => Task.CompletedTask;
}

Usage Examples

MyEmailSender.cs

using Azure.Identity;

namespace MyNamespace;

public class MyEmailSender : IEmailSender
{
    private readonly EmailClient _client;

    public MyEmailSender(IConfiguration config)
    {
        var credential = new ChainedTokenCredential(
            new ClientSecretCredential( 
                _config["AZURE_TENANT_ID"],
                _config["AZURE_CLIENT_ID"],
                _config["AZURE_CLIENT_SECRET"]),
            new ManagedIdentityCredential()
        )
        _client = new EmailClient(new Uri("https://my-instance.communication.azure.com/")
    }

    public Task SendEmailAsync(string email, string subject, string message)
    {
        var recipients = new EmailRecipients(new [] { new EmailAddress(email) });
        var content = new EmailContent(subject)
        {
            PlainText = message
        };

        await _client.SendAsync(new EmailMessage("[email protected]", content, recipients);
    }

    public Task SendConfirmationLinkAsync<TUser>(TUser user, string email, string confirmationLink) where TUser : class
    {
        return SendEmailAsync(email, "Confirm your email for MyWebSite", $"Please confirm your MyWebSite account by <a href='{confirmationLink}'>clicking here</a>.");
    }

    public Task SendPasswordResetLinkAsync<TUser>(TUser user, string email, string resetLink) where TUser : class
    {
        return SendEmailAsync(email, "Reset your password for MyWebSite", $"Please reset your MyWebSite password by <a href='{resetLink}'>clicking here</a>.");
    }

    public Task SendPasswordResetCodeAsync<TUser>(TUser user, string email, string resetCode) where TUser : class
    {
        return SendEmailAsync(email, "Reset your password for MyWebSite", $"Reset your MyWebSite password using the following code: {resetCode}");
    }
}

Program.cs

// ...
builder.Services.AddSingleton<IEmailSender, MyEmailSender>();
// ...

Alternative Designs

  • We could not add the DIMs and instead force customers who care to parse the subject and htmlMessage to figure out what kind of email is being sent and try to extract any links or password reset codes.
  • We could omit the TUser user parameters since it's unneeded by the default implementation, but it seems like it could be useful and it is not difficult to pass in.

Risks

In the future, we might not need to send these exact kinds of emails. Maybe the code in Identity UI or MapIdentityApi will have the need to specify more parameters than just the recipient and email confirmation link. In that case, we'd probably add more DIMs, but it would be less clear which ones are actually used.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-identityIncludes: Identity and providers

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions