Account confirmation and password recovery in ASP.NET Core Blazor
This article explains how to configure an ASP.NET Core Blazor Web App with email confirmation and password recovery.
Namespace
The app's namespace used by the example in this article is BlazorSample
. Update the code examples to use the namespace of your app.
Select and configure an email provider
In this article, Mailchimp's Transactional API is used via Mandrill.net to send email. We recommend using an email service to send email rather than SMTP. SMTP is difficult to configure and secure properly. Whichever email service you use, access their guidance for .NET apps, create an account, configure an API key for their service, and install any NuGet packages required.
Create a class to fetch the secure email API key. The example in this article uses a class named AuthMessageSenderOptions
with a EmailAuthKey
property to hold the key.
AuthMessageSenderOptions
:
namespace BlazorSample;
public class AuthMessageSenderOptions
{
public string? EmailAuthKey { get; set; }
}
Register the AuthMessageSenderOptions
configuration instance in the Program
file:
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);
Configure a user secret for the provider's security key
Set the key with the Secret Manager tool. In the following example, the key name is EmailAuthKey
, and the key is represented by the {KEY}
placeholder. In a command shell, navigate to the app's root folder and execute the following command with the API key:
dotnet user-secrets set "EmailAuthKey" "{KEY}"
For more information, see Safe storage of app secrets in development in ASP.NET Core.
Warning
Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is always insecure. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the Secret Manager tool is recommended for securing sensitive data. For more information, see Securely maintain sensitive data and credentials.
Implement IEmailSender
Implement IEmailSender
for the provider. The following example is based on Mailchimp's Transactional API using Mandrill.net. For a different provider, refer to their documentation on how to implement sending a message in the Execute
method.
Components/Account/EmailSender.cs
:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Mandrill;
using Mandrill.Model;
using BlazorSample.Data;
namespace BlazorSample.Components.Account;
public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
ILogger<EmailSender> logger) : IEmailSender<ApplicationUser>
{
private readonly ILogger logger = logger;
public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value;
public Task SendConfirmationLinkAsync(ApplicationUser user, string email,
string confirmationLink) => SendEmailAsync(email, "Confirm your email",
"Please confirm your account by " +
$"<a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email,
string resetLink) => SendEmailAsync(email, "Reset your password",
$"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email,
string resetCode) => SendEmailAsync(email, "Reset your password",
$"Please reset your password using the following code: {resetCode}");
public async Task SendEmailAsync(string toEmail, string subject, string message)
{
if (string.IsNullOrEmpty(Options.EmailAuthKey))
{
throw new Exception("Null EmailAuthKey");
}
await Execute(Options.EmailAuthKey, subject, message, toEmail);
}
public async Task Execute(string apiKey, string subject, string message,
string toEmail)
{
var api = new MandrillApi(apiKey);
var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail,
subject, message);
await api.Messages.SendAsync(mandrillMessage);
logger.LogInformation("Email to {EmailAddress} sent!", toEmail);
}
}
Note
Body content for messages might require special encoding for the email service provider. If links in the message body can't be followed, consult the service provider's documentation.
Configure app to support email
In the Program
file, change the email sender implementation to the EmailSender
:
- builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
+ builder.Services.AddSingleton<IEmailSender<ApplicationUser>, EmailSender>();
Remove the IdentityNoOpEmailSender
(Components/Account/IdentityNoOpEmailSender.cs
) from the app.
In the RegisterConfirmation
component (Components/Account/Pages/RegisterConfirmation.razor
), remove the conditional block in the @code
block that checks if the EmailSender
is an IdentityNoOpEmailSender
:
- else if (EmailSender is IdentityNoOpEmailSender)
- {
- ...
- }
Also in the RegisterConfirmation
component, remove the Razor markup and code for checking the emailConfirmationLink
field, leaving just the line instructing the user to check their email ...
- @if (emailConfirmationLink is not null)
- {
- ...
- }
- else
- {
<p>Please check your email to confirm your account.</p>
- }
@code {
- private string? emailConfirmationLink;
...
}
Email and activity timeout
The default inactivity timeout is 14 days. The following code sets the inactivity timeout to five days with sliding expiration:
builder.Services.ConfigureApplicationCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromDays(5);
options.SlidingExpiration = true;
});
Change all ASP.NET Core Data Protection token lifespans
The following code changes Data Protection tokens' timeout period to three hours:
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
options.TokenLifespan = TimeSpan.FromHours(3));
The built in Identity user tokens (AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs) have a one day timeout.
Note
Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).
Change the email token lifespan
The default token lifespan of the Identity user tokens is one day.
Note
Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).
To change the email token lifespan, add a custom DataProtectorTokenProvider<TUser> and DataProtectionTokenProviderOptions:
CustomTokenProvider.cs
:
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace BlazorSample;
public class CustomEmailConfirmationTokenProvider<TUser>
: DataProtectorTokenProvider<TUser> where TUser : class
{
public CustomEmailConfirmationTokenProvider(
IDataProtectionProvider dataProtectionProvider,
IOptions<EmailConfirmationTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<TUser>> logger)
: base(dataProtectionProvider, options, logger)
{
}
}
public class EmailConfirmationTokenProviderOptions
: DataProtectionTokenProviderOptions
{
public EmailConfirmationTokenProviderOptions()
{
Name = "EmailDataProtectorTokenProvider";
TokenLifespan = TimeSpan.FromHours(4);
}
}
public class CustomPasswordResetTokenProvider<TUser>
: DataProtectorTokenProvider<TUser> where TUser : class
{
public CustomPasswordResetTokenProvider(
IDataProtectionProvider dataProtectionProvider,
IOptions<PasswordResetTokenProviderOptions> options,
ILogger<DataProtectorTokenProvider<TUser>> logger)
: base(dataProtectionProvider, options, logger)
{
}
}
public class PasswordResetTokenProviderOptions :
DataProtectionTokenProviderOptions
{
public PasswordResetTokenProviderOptions()
{
Name = "PasswordResetDataProtectorTokenProvider";
TokenLifespan = TimeSpan.FromHours(3);
}
}
Configure the services to use the custom token provider in the Program
file:
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
new TokenProviderDescriptor(
typeof(CustomEmailConfirmationTokenProvider<ApplicationUser>)));
options.Tokens.EmailConfirmationTokenProvider =
"CustomEmailConfirmation";
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services
.AddTransient<CustomEmailConfirmationTokenProvider<ApplicationUser>>();
Troubleshoot
If you can't get email working:
- Set a breakpoint in
EmailSender.Execute
to verifySendEmailAsync
is called. - Create a console app to send email using code similar to
EmailSender.Execute
to debug the problem. - Review the account email history pages at the email provider's website.
- Check your spam folder for messages.
- Try another email alias on a different email provider, such as Microsoft, Yahoo, or Gmail.
- Try sending to different email accounts.
Warning
Do not use production secrets in test and development. If you publish the app to Azure, set secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables.
Enable account confirmation after a site has users
Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches:
- Update the database to mark all existing users as confirmed.
- Confirm existing users. For example, batch-send emails with confirmation links.
Additional resources
ASP.NET Core