When you're implementing your Identity Server application in C# you will most likely need to implement some forgot password and reset password functionality.
The usual and best way to implement this is to allow the user to enter an email or username into a Forgot password page -then have Identity Server email a link to the users email (on verification of it being valid).
When you select the scaffold for the forgot and reset password functionality from Identity Server you get a stub for the IEmailSender interface override.
Before we go any further. If you don't know at this point - IEmailSender is the interface provided from Microsoft to override and implement you email send functionality.
If you take a look at the IEmailSender Microsoft WebPage then you see it's include and definition is as follows:
Namespace: Microsoft.AspNetCore.Identity.UI.Services
public interface IEmailSender
As part of your definition, you need to override the following method:
SendEmailAsync(String, String, String)
The inputs for this method are:
Now that we've giving a little definition on what we need to create, let's start to take a look at the code.
One more thing to note. To make sure we have this configurable, we will be putting the email server details into our appsettings.json and reading them into a data model that we will call EmailSettings.
One more thing to note before moving forward. This example will be using a SMTP Client. Microsoft has declared that their System.Net.Mail SmtpClient is no Obsolete, and so we will be using MailKit and MimeKit packages for this example.
You can download these into your project using NuGet from within your DevStudio tool.
If you started with the scaffold and added the forgot and reset email pages from there, you should see a new class and file called EmailSender.cs. If you don't then create this file now.
The initial autogenerated code should look something like the following:
public interface IEmailSender {
Task SendEmailAsync(
string email, string subject, string message);
}
public class EmailSender : IEmailSender {
public Task SendEmailAsync(string email, string subject, string message)
{
return Task.CompletedTask;
}
}
This is the code we will be changing to be able to send our email. Before we do this, let's update all the other places in our code we need to, so as to call this Async email sender code.
As I mentioned above. We want to be able to update the email settings outside of the actual code, and so appsettings.json is the perfect place.
To achieve this, we need to add a few elements. First, we need to create our data model.
You can do this by either creating a new file called EmailSettings.cs (create this somewhere such as Models or Entities folders). The other option, the one I prefer to use, is to add this data model class into your EmailSender.cs file. I like to keep all the related code together.
However, you want to structure your code is your own personal preference.
The data model I use is as follows:
public class EmailSettings {
public string MailServer { get; set; }
public int MailPort { get; set; }
public bool UseSSL { get; set; }
public string SenderName { get; set; }
public string Sender { get; set; }
public string Password { get; set; }
}
You can probably tell from the above, this will include all the details you need to connect to your server. I like to include a useSSL flag in my code (I've seen other examples that don't) as I have used servers such as smtp.office365.com that you need to set the flag to false for it to work - at least in my case.
Next, you need to update your Startup.cs file. Make a change in
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
This code gets your configuration from the appsettings.json file and makes it available for you to use.
There is one more change you need to do. At the bottom of ConfigServices you need to add the following line of code:
services.AddSingleton<IEmailSender, EmailSender>();
This will create a singleton using your EmailSender code.
Note: You will need to use
private readonly ILogger _logger; private readonly IEmailSender _emailSender;
private readonly ILogger<HomeController> _logger;
private readonly IEmailSender<EmailSender> _emailSender;
public HomeController(ILogger<HomeController> logger, IEmailSender<EmailSender> emailSender)
{
_logger = logger;
_emailSender = emailSender;
}
To allow us to pass the package into the EmailSender controller code, we need to create the ForgotPassword model.
First, create a new model called ForgotPasswordModel.cs
Once done add the following code to the class:
public string Email { get; set; }
public string uUsername { get; set; }
public string ReturnUrl { get; set; }
These values are required for us to pass in the information we require. Note: ReturnUrl is required if you want to return to the URL that you have your forgot password link.
The webpage definition can be taken from the default forgot password page. This page simply has to have a field to enter the email address (or username) of the person looking to reset the password, and a submit button.
This page would look something similar to:
@model ForgotPasswordModel
@{
ViewData["Title"] = "Forgot your password?";
}
<h1>@ViewData["Title"]</h1>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
The above is the default page taken from the scaffolding for ForgotPassword. This is just an example for you to look at.
To give some extra info for thought. As part of the button submit - you could use asp-action to ensure your ForgotPassword method is called.
If you're using the default scaffold code then you will have the default PageModel with OnPostAsync code. However, if you want to add your
public async Task SendEmailAsync(string email, string subject, string message)
{
try
{
var mimeMessage = new MimeMessage();
mimeMessage.From.Add(new MailboxAddress(_emailSettings.SenderName,
_emailSettings.Sender));
mimeMessage.To.Add(new MailboxAddress(email));
mimeMessage.Subject = subject;
mimeMessage.Body = new TextPart("html")
{
Text = message
};
using (var client = new SmtpClient())
{
// For demo-purposes, accept all SSL certificates (in case the server
supports STARTTLS)
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
if (_env.IsDevelopment())
{
// The third parameter is useSSL (true if the client should make an
SSL-wrapped
// connection to the server; otherwise, false).
await client.ConnectAsync(_emailSettings.MailServer,
_emailSettings.MailPort, _emailSettings.UseSSL);
}
else
{
await client.ConnectAsync(_emailSettings.MailServer);
}
// Note: only needed if the SMTP server requires authentication
await client.AuthenticateAsync(_emailSettings.Sender,
_emailSettings.Password);
await client.SendAsync(mimeMessage);
await client.DisconnectAsync(true);
}
}
catch (Exception ex)
{
// TODO: handle exception
throw new InvalidOperationException(ex.Message);
}
}
As part of this walk-through I'm not going to include any code relating to your forgot password page as this is beyond the scope of this post, and everyone may want to do this different.
I'd suggest to start with the scaffolding and go from there.
This post is about implementing the email sender code to use in your forgot password page; your email sender code. You could also use this code in any other controllers where you want to send emails to a user's email address.
I hope this proves useful in your coding endeavors.
Hi,
how does the constructor has to look like so SendEmailAsync can access _emailSettings ?
Does this still work? This the current best practice?
This post was written a few years ago now and the language itself has moved on. To ve honest I've been working in other languages of late so have not revisited these posts so I would just take this as a baseline and take a look at what is best practice today.