dotnet / dotnet/aspnetcore

The SendConfirmationLinkAsync is sending confirmationLink to non-public address

Open
#63,142 4 comments 0 reactions 0 assignees View on GitHub
area-identity
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [x] 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.

https://github.com/dotnet/aspnetcore/blob/b52c80fef2b8bf5239c7643d9c79d1029bb6c988/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs#L390

### Expected Behavior

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

For example
```
public interface IIdentityCoreHandler 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 : IIdentityCoreHandler
where TUser : class
{
/*private readonly IPublisher _eventPublisher;*/
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEmailSender _emailSender;
private readonly LinkGenerator _linkGenerator;

public IdentityCoreHandler(/*IPublisher eventPublisher,*/ IHttpContextAccessor httpContextAccessor, IEmailSender 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(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 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>();
into
var identityCoreHandler = endpoints.ServiceProvider.GetRequiredService>();

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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.