Skip to content

The SendConfirmationLinkAsync is sending confirmationLink to non-public address #63142

@JJong-nl

Description

@JJong-nl

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When we host the IdentityApi in a non-public environment and we call an api-method for sending the conformation.
The conformationlink has the host address of the api application.

async Task SendConfirmationEmailAsync(TUser user, UserManager<TUser> userManager, HttpContext context, string email, bool isChange = false)

Expected Behavior

That an Event is called/raised with the parameters to be processed. (user, userid, code)

For example

    public interface IIdentityCoreHandler<TUser> where TUser : class
    {
        Task LoggedIn(string email);
        Task LoggedInFailure(string email, string errorMessage);
        Task EmailConfirmed(TUser user);
        Task SendConfirmationEmailAsync(TUser user, string? userId, string email, string code, bool isChange = false);
        Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode);
    }

with implementation

public class IdentityCoreHandler<TUser> : IIdentityCoreHandler<TUser>
    where TUser : class
{
    /*private readonly IPublisher _eventPublisher;*/
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IEmailSender<TUser> _emailSender;
    private readonly LinkGenerator _linkGenerator;

    public IdentityCoreHandler(/*IPublisher eventPublisher,*/ IHttpContextAccessor httpContextAccessor, IEmailSender<TUser> emailSender, LinkGenerator linkGenerator)
    {
        /*_eventPublisher = eventPublisher;*/
        _httpContextAccessor = httpContextAccessor;
        _emailSender = emailSender;
        _linkGenerator = linkGenerator;
    }

    public Task EmailConfirmed(TUser user)
        => Task.CompletedTask;

    // we could do extra checks for success login
    // Maybe first login from 'unknown' location? Than we could notify the user
    public Task LoggedIn(string email)
        => Task.CompletedTask;

    public Task LoggedInFailure(string email, string errorMessage)
        => Task.CompletedTask;

    public async Task SendConfirmationEmailAsync(TUser user, string? userId, string code, string email, bool isChange = false)
    {
        code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
        var routeValues = new RouteValueDictionary()
        {
            ["userId"] = userId,
            ["code"] = code,
        };

        if (isChange)
        {
            // This is validated by the /confirmEmail endpoint on change.
            routeValues.Add("changedEmail", email);
        }

        var confirmEmailUrl = _linkGenerator.GetUriByName(_httpContextAccessor.HttpContext!, ConfirmEmailEndpointStore.ConfirmEmailEndpointName, routeValues)
            ?? throw new NotSupportedException($"Could not find endpoint named '{ConfirmEmailEndpointStore.ConfirmEmailEndpointName}'.");

        // When developer/usecase wants to create his own IIdentityHandler, he can inject IConfiguration
        // and set a different host for the confirmEmailUrl by a appsettings.json key/value

        await _emailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(confirmEmailUrl));
    }

    public async Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode)
    {
        //we could publish an event with mediatr, and let the domain solve that to do with it..
        //var @event = new Domain.Model.Identity.Events.SendPasswordResetCodeEvent<TUser>(user, email, resetCode);
        //await _eventPublisher.Publish(@event);
        await Task.CompletedTask;
    }
}

than we can change the SendConfirmationEmailAsync in the IdentityApiEndpointRouteBuilderExtensions to

            async Task SendConfirmationEmailAsync(TUser user, UserManager<TUser> userManager, HttpContext context, string email, bool isChange = false)
            {
                var code = isChange
                    ? await userManager.GenerateChangeEmailTokenAsync(user, email)
                    : await userManager.GenerateEmailConfirmationTokenAsync(user);
                var userId = await userManager.GetUserIdAsync(user);

                await identityCoreHandler.SendConfirmationEmailAsync(user, userId, code, email, isChange: isChange);
            }

change line 47
var emailSender = endpoints.ServiceProvider.GetRequiredService<IEmailSender>();
into
var identityCoreHandler = endpoints.ServiceProvider.GetRequiredService<IIdentityCoreHandler>();

Now we have full controll what we want to do in every scanario

Steps To Reproduce

Host the IdentityApi in a non-public environment.

Exceptions (if any)

No response

.NET Version

No response

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-identityIncludes: Identity and providers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions