Being able to have users create accounts on your website is the first step in creating a service that you can make available online.
Although a seemingly mundane task it involves a lot of work and is easy to get wrong. It is possible to use the templates that come with Visual Studio (or yeoman if you are not using Windows), but event those leave you with a halfway solution, where you have to set up email confirmation yourself.
The templates try to hide away all the details. However, if something goes wrong and you don’t understand those details it’s very hard to find the source of the problem.
This blog post is a step by step guide on how to setup an ASP.NET Core website from scratch (starting from an empty web application) where users can create accounts, receive an email for email address confirmation, and also provide the ability for password reset using ASP.NET Identity Core. The end result is available here.
You can use git to get a copy of the end result:
$ git clone https://github.com/ruidfigueiredo/AspNetIdentityFromScratch
This guide assumes you are using Visual Studio Code so that it can be followed in Windows, Mac or Linux. If you are using the full version of Visual Studio you should not have any problems following the instructions.
I’m assuming that you have .Net Core installed. If you are not running the full version of Visual Studio install npm, yeoman and the yeoman generator for asp.net.
1. Create an empty web application
To create an empty web application run the following command:
$ yo aspnet
_-----_ ╭──────────────────────────╮
| | │ Welcome to the │
|--(o)--| │ marvellous ASP.NET Core │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of application do you want to create? (Use arrow keys)
❯ Empty Web Application
Console Application
Web Application
Web Application Basic [without Membership and Authorization]
Web API Application
Nancy ASP.NET Application
Class Library
Unit test project (xUnit.net)
Pick Empty Web Application. We’ll start with the empty web application so that we can describe all the steps in their simplest form, making this whole process easier to understand.
Because we are starting with the empty web application we have to add MVC as a dependency and configure it. So lets do that. Open project.json and add in the dependencies section:
"Microsoft.AspNetCore.Mvc": "1.0.1"
"Microsoft.AspNetCore.StaticFiles": "1.1.0"
When you are using Visual Studio Code (or full version) you can get intellisense while you are typing the NuGet package name, and also for the version. 1.0.1 was the latest version at the time I wrote this (15/11/2016). I’ve also added StaticFiles which enables serving files from the web root folder (wwwroot).
Open Startup.cs and add the IoC configuration required to use MVC.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
Also, update the Configure method so that the MVC middleware is added to the pipeline with the default route. We’ve also added the StaticFiles middleware, which has to be before MVC.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
If you are not familiar with middleware and IoC in ASP.NET Core then the middleware documentation page and the Dependency Injection documentation pages are good resources to get up to speed about them.
Because we are using the default route, if an http request is made to the root of our website the controller that is going to be invoked is HomeController. The controller action will be Index. So let’s create those.
First create a Controllers’ folder (the empty template does not have one). Then use yeoman to generate a new controller (run this command inside the folder Controllers)
$ yo aspnet:mvccontroller Home
You can get a list of all available yeoman subgenerators by running yo aspnet --help
.
We also need to create the Views folder and, to make our views simpler we’ll also create a Layout view.
Inside the Views folder create another folder named Shared. Inside that folder use yeoman to generate an html page:
$ yo aspnet:htmlpage _Layout
This will create a _Layout.html file. Rename it to _Layout.cshtml.
Inside _Layout.cshtml find the body tag and inside add
@RenderBody()
The reason why we are doing this is because there’s no option to generate a Layout page if you are not using full Visual Studio. The htmlpage subgenerator comes pretty close. You just need to rename the file extension to .cshtml and add the instructor to render the actual view that is supposed to be rendered.
The _Layout.cshtml file on its own has no effect though. In MVC, there’s a special file that is run automatically for the views. That file is _ViewStart.cshtml. Create it in the Views folder and add this to it:
@{
Layout = "_Layout";
}
That sets the default layout to the one we’ve just created.
Also, because later on we’ll be using a new feature of ASP.NET Core named tag helpers we’ll enable them for all the views using another new feature which is a file name _ViewImports.cshtml. Create the _ViewImports.cshtml file in the Views folder and add this inside:
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
_ViewImports.cshtml allows us to add @using
statements, for example @using System.Collections.Generic
so that all razor views in the folder where _ViewImports is and its subfolders don’t have to add that statement. We can also use it with @addTagHelper
, which we did here so that all the views can use tag helpers.
We still need the view for the Index action. Create a folder named Home inside Views and inside it create a file named Index.cshtml. Add this as its contents:
<h1>Home</h1>
@if (User.Identity.IsAuthenticated)
{
<p>User @User.Identity.Name is signed in. <a href="/Account/Logout">Logout</a> </p>
}
else
{
<p>No user is signed in.</p>
}
<div>
<a href="/Account/Login">Login</a>
</div>
<div>
<a href="/Account/Register">Register</a>
</div>
<div>
<a href="/Account/ForgotPassword">Forgot Password</a>
</div>
Run the project and you should see a very simple screen alerting you for the fact that no user has signed in. The links won’t work yet, we’ll address that later.
ASP.NET Core Identity
ASP.NET Core Identity is the membership system for ASP.NET Core. It provides the functionality necessary to manage user accounts. By using it we will be able to create users and generate tokens for email confirmation and password reset.
To add it to the project open project.json and add these two NuGet packages:
"Microsoft.AspNetCore.Identity": "1.1.0",
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.1.0"
(You actually only need to add Microsoft.AspNetCore.Identity.EntityFrameworkCore
since it has a dependency on Microsoft.AspNetCore.Identity
so both will be fetched anyway)
Before we continue further, a note about how ASP.NET Core Identity stores your users. A user will have (at least) all the properties contained in a class named IdentityUser. You can create a subclass of IdentityUser and add more properties. Those will be present in the user’s table when we actually create the database. For this example we will just use the default IdentityUser.
Because we need a database to store our membership data we’ll use SQLite (if you you are interested in using a different database check this post: Cross platform database walk-through using ASP.NET MVC and Entity Framework Core).
To add SQLite to the project edit project.json’s dependencies and add:
"Microsoft.EntityFrameworkCore.Sqlite": "1.1.0"
Also, we will need the command line tools for Entity Framework, so add to project.json’s dependencies:
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
And to the tools section:
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
(No, it’s not a mistake, you need to add the dependency in both places)
Configuring ASP.NET Core Identity
The common way to use Identity assumes you will be inheriting from a set of base classes, namely IdentityUser, so that you can specify what extra information you want to save about your users. Also, IdentityDbContext, which your own application’s DbContext is supposed to inherit from and then specify your own extra DbSets.
This poses a dilemma. There are very good reasons for not doing this. First, ASP.NET Core Identity is not the only membership system out there, and not only that, it has changed significantly between versions. So, if you fully commit by tying your user classes and DbContext with ASP.NET Core Identity, you are actually tying them to a particular version of Identity.
It is a tiny bit extra work to get all of this working without tying your project to Identity. We’ll do that here.
First we’ll need to change Startup.cs and add the necessary IoC configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlite("Data Source=users.sqlite",
optionsBuilder => optionsBuilder.MigrationsAssembly("AspNetIdentityFromScratch")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();
}
We are registering IdentityDbContext
and using Data Source=users.sqlite
as the connection string. When we create the database a file named users.sqlite will be created in the project’s output directory (more on this shortly).
It is important to note the second parameter used in the UseSqlite
extension method:
optionsBuilder => optionsBuilder.MigrationsAssembly("AspNetIdentityFromScratch")
That is required because when we run the tooling (dotnet ef) to create the database migration a check will be performed to validate if the IdentityDbContext class is in the same assembly as the project where you are running the tooling. If it’s not, which is the case here, you’ll get an error:
Your target project 'AspNetIdentityFromScratch' doesn't match your migrations assembly 'Microsoft.AspNetCore.Identity.EntityFrameworkCore'
...
That’s why we need to use that second argument to UseSqlite
. AspNetIdentityFromScratch
is the name I used for the project, you need to update that to the name you used for yours.
Next is the actual registration for Identity:
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();
We do that by specifying the class we want to use for users, IdentityUser
, and roles, IdentityRole
. As previously mentioned you can specify your own sublcass of IdentityUser
, however if you do, you also need to use to change how the IdentityDbContext is registered, for example:
services.AddDbContext<IdentityDbContext<YourCustomUserClass>>(...
services.AddIdentity<YourCustomUserClass, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext<YourCustomUserClass>>(...
You probably noticed that I did not mention anything about IdentityRole
. You can mostly ignore this since you can add roles as Claims. I suspect that this is included just because in old membership systems the concept of Role was very prominent. Not so much when you have claims. Also, specifying a custom IdentityRole
complicates things a lot. I’ll show you how you can add Roles to your users using claims instead.
The next part in the IoC registration of Identity is:
.AddEntityFrameworkStores<IdentityDbContext>()
This is simply specifying which DbContext to use, and finally:
.AddDefaultTokenProviders();
The token providers are the components that generate the confirm email and reset password tokens that we’ll use later.
Now we just need to update the pipeline:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...
app.UseIdentity();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
A note about what happens when we use the UseIdentity
extension method. What this actually does is it sets up the cookie middleware. What the cookie middleware does is it redirects the user to the login page when a controller action in MVC returns a 401 response, and after the user is signed in and the authentication cookie is created it converts it to the ClaimsPrincipal and ClaimsIdentity that you can access in a controller action when you do a Request.User
.
Generate the database to store membership data
With this new version of Entity Framework there’s no way to generate the database without using migrations. We need to create a migration to then have the tooling generate the database.
To create a migration in the command line:
$ dotnet ef migrations add Initial
This will create a migration named “Initial”. To apply it:
$ dotnet ef database update
What this command does is apply all pending migrations, in this case it will only be “Initial”, which will create all the required tables for ASP.NET Core Identity.
You should now have a file named users.sqlite in your output folder.
It is possible to generate the database programmatically (but you always need to create the migration first). This way if you want to share your project with someone else, they won’t have to run dotnet ef database update
before being able to run the project.
To do that, in Startup.cs’ Configuration
method add:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IdentityDbContext dbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
dbContext.Database.Migrate(); //this will generate the db if it does not exist
}
//...
}
Be sure to run in with the environment set to “development”. Usually this is the case if you run from Visual Studio, but if you decide to run form the command line:
$ dotnet run --environment "Development"
Sending emails
To enable email verification and password resets we need to be able to send emails. The easiest thing to do, especially when you are developing, is to just have the email messages saved to a file on disk. This saves you the time of having to wait for the email when you are trying things out.
First thing we need to do is to create an interface. Let’s name it IMessageService:
public interface IMessageService
{
Task Send(string email, string subject, string message);
}
And now our “writes to file” implementation:
public class FileMessageService : IMessageService
{
Task IMessageService.Send(string email, string subject, string message)
{
var emailMessage = $"To: {email}\nSubject: {subject}\nMessage: {message}\n\n";
File.AppendAllText("emails.txt", emailMessage);
return Task.FromResult(0);
}
}
The last thing is to register this in ConfigureServices
in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddTransient<IMessageService, FileMessageService>();
}
User registration page
For this walk-through we’ll be building the simplest possible razor views since the focus is in how to use ASP.NET Core Identity.
To continue create the AccountController in the Controllers’ folder.
$ yo aspnet:mvccontroller AccountController
In the constructor add the UserManager<IdentityUser>
and SignInManager<IdentityUser>
(these dependencies were registered when we added services.AddIdentity<...>
in Startup.cs’s ConfigureServices) and IMessageService
:
public class AccountController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IMessageService _messageService;
public AccountController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IMessageService messageService)
{
this._userManager = userManager;
this._signInManager = signInManager;
this._messageService = messageService;
}
//...
The UserManager
is what we’ll be using to create users and generate validation tokens. SignInManager
allows us to do password validation and sign in/out the users by managing the authentication cookies for us.
Create a Register method:
public IActionResult Register()
{
return View();
}
And now the razor file in Views/Account/Register.cshtml:
<form method="POST">
<div>
<label >Email</label>
<input type="email" name="email"/>
</div>
<div>
<label>Password</label>
<input type="password" name="password"/>
</div>
<div>
<label>Retype password</label>
<input type="password" name="repassword"/>
</div>
<input type="submit"/>
</form>
<div asp-validation-summary="All"></div>
The only thing remarkable about this view is the asp-validation-summary tag helper. Tag helpers are a new feature available in ASP.NET Core. They are an alternative the old HTML helpers.
The validation summary tag helper, set with the value of “All”, will display all model errors. We’ll use it to display any errors we detect when creating the user (for example, username already taken or passwords don’t match).
We are now ready to create the action method for the HTTP POST of the user registration form we’ve just created:
[HttpPost]
public async Task<IActionResult> Register(string email, string password, string repassword)
{
if (password != repassword)
{
ModelState.AddModelError(string.Empty, "Password don't match");
return View();
}
var newUser = new IdentityUser
{
UserName = email,
Email = email
};
var userCreationResult = await _userManager.CreateAsync(newUser, password);
if (!userCreationResult.Succeeded)
{
foreach(var error in userCreationResult.Errors)
ModelState.AddModelError(string.Empty, error.Description);
return View();
}
var emailConfirmationToken = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
var tokenVerificationUrl = Url.Action("VerifyEmail", "Account", new {id = newUser.Id, token = emailConfirmationToken}, Request.Scheme);
await _messageService.Send(email, "Verify your email", $"Click <a href=\"{tokenVerificationUrl}\">here</a> to verify your email");
return Content("Check your email for a verification link");
}
To create our new user we first had to create an instance of IdentityUser
. Because we’re using the email as the username we’ve set both to the same value.
We then called _userManager.CreateAsync(newUser, password);
which returns an object with a boolean property named Success
. We should query it to check if the user was created successfully. If not, there’s another property we should check, named Errors
, which is a list of reasons for why the user creation failed (e.g. password requirements not met, username already taken, etc).
At this point, if userCreatingResult.Success
is true, the user is already created. However, if you were to check newUser.EmailConfirmed
it would return false.
Because we want to validate emails we can generate an email confirmation token using _userManager
‘s GenerateEmailConfirmationTokenAsync
. We then use the IMessagingService
to “send” it.
If you run the project now and go to the account/register
url to create a new user, after the process you should have a file named emails.txt in your project folder with the email confirmation url.
When we create the email confirmation controller action you can just copy that email confirmation url and have the email verified (we’ll do that after the roles).
Adding roles as claims
I mentioned you didn’t need to pay too much attention to IdentityRole
because you could add a role as a claim.
Here’s an example of how to add the “Administrator” role to a user:
await _userManager.AddClaimAsync(identityUser, new Claim(ClaimTypes.Role, "Administrator"));
You can then “require” in a controller action in the usual way:
[Authorize(Roles="Administrator")]
public IActionResult RequiresAdmin()
{
return Content("OK");
}
Email Verification
Let’s create the controller action that will be invoked when the user clicks on the link in the email:
public async Task<IActionResult> VerifyEmail(string id, string token)
{
var user = await _userManager.FindByIdAsync(id);
if(user == null)
throw new InvalidOperationException();
var emailConfirmationResult = await _userManager.ConfirmEmailAsync(user, token);
if (!emailConfirmationResult.Succeeded)
return Content(emailConfirmationResult.Errors.Select(error => error.Description).Aggregate((allErrors, error) => allErrors += ", " + error));
return Content("Email confirmed, you can now log in");
}
We need to load the IdentityUser
using the user’s id
. With that and the email confirmation token we can call userManager
‘s ConfirmEmailAsync
method. If the token is correct the EmailConfirmed
property in the users’ database table will be updated to indicate the user has a confirmed email address.
We are using the Controller
‘s Content
method for simplicity. You could instead create a new view describing that the email is confirmed with a link to the login page. One thing you shouldn’t do is log the user in automatically after this. That’s because no error will be raised if the user clicks the confirmation link multiple times.
If you were to log in the user automatically after email confirmation, that link would essentially become a way to log in without specifying a password, which could be used multiple times.
Login page
First create a Login method in the AccountController to handle GET requests:
public IActionResult Login()
{
return View();
}
And it’s razor view in Views/Account/Login.cstml
:
<form method="POST">
<div>
<label>Email</label>
<input type="email" name="email"/>
</div>
<div>
<label>Password</label>
<input type="password" name="password"/>
</div>
<input type="submit"/>
</form>
<div asp-validation-summary="All"></div>
And now the controller action to handle the POST request:
[HttpPost]
public async Task<IActionResult> Login(string email, string password, bool rememberMe)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid login");
return View();
}
if (!user.EmailConfirmed)
{
ModelState.AddModelError(string.Empty, "Confirm your email first");
return View();
}
var passwordSignInResult = await _signInManager.PasswordSignInAsync(user, password, isPersistent: rememberMe, lockoutOnFailure: false);
if (!passwordSignInResult.Succeeded)
{
ModelState.AddModelError(string.Empty, "Invalid login");
return View();
}
return Redirect("~/");
}
Here we are using UserManager
to retrieve the user by email (FindByEmailAsync
). If the email isn’t confirmed we are not allowing the user to log in. What you can do in this situation is provide an option to re-send the verification email.
To actually sign the user in we’ll use SignInManager
. Its PasswordSignInAsync
method requires the IdentityUser
, the correct password, a flag isPersistent
and another flag lockoutOnFailure
.
What this method does is issue the creation of the encrypted cookie that will sit at the user’s machine and which will contain the user’s claims. After you’ve successfully logged in you can inspect this cookie using Chrome’s Developer tools
The isPersistent
parameter will determine the value of the cookie’s Expires property. When set to true (which was the case in the screenshot) the cookie will have an expiration date of a couple of months into the future (this timespan is configurable, see configuration section). When set to false the cookie will have the Expires property set to session
which means the cookie will be deleted after the user closes the browser.
The lockoutOnFailure
flag allows us to stop the user from logging in if there were too many failed log in attempts. How many, and for how long the user is locked out is configurable (we’ll mention this in the configuration section).
If you do decide to use lockoutOnFailure
be aware that you need to call _userManager.AccessFailedAsync(user)
every time the user fails to log in.
There’s one thing I left out so that this example was simpler, and that was the returnUrl
. If you want you can add a parameter to the Login method so that when the user successfully logs in you can issue a redirect to the return url. Just be aware that you should check if the url is local (i.e. points to your application and not someone else’s). To do that, in the controller action, perform the following test:
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}else
{
return Redirect("~/");
}
Password reset
For resetting passwords we have to create a controller action with a view that renders a form for the user to input his/her email. Let’s do that.
First the controller action, let’s call it ForgotPassword:
public IActionResult ForgotPassword()
{
return View();
}
And the view in /Views/Account/ForgotPassword.cshtml:
<form method="POST">
<div>
<label>Email</label>
<input type="email" name="email"/>
</div>
<input type="submit"/>
</form>
And the action to deal with the POST of the form:
[HttpPost]
public async Task<IActionResult> ForgotPassword(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
return Content("Check your email for a password reset link");
var passwordResetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
var passwordResetUrl = Url.Action("ResetPassword", "Account", new {id = user.Id, token = passwordResetToken}, Request.Scheme);
await _messageService.Send(email, "Password reset", $"Click <a href=\"" + passwordResetUrl + "\">here</a> to reset your password");
return Content("Check your email for a password reset link");
}
You’ll probably noticed that the user will see the same message regardless of the email belonging to an existing account. You should do that because if you don’t, this functionality can be used to discover if a user has an account in your site.
The rest of the controller action is just generating the token and sending the email.
We still need to build the ResetPassword controller action where we are sending the users from the link in the email, so let’s do that:
[HttpPost]
public async Task<IActionResult> ResetPassword(string id, string token, string password, string repassword)
{
var user = await _userManager.FindByIdAsync(id);
if (user == null)
throw new InvalidOperationException();
if (password != repassword)
{
ModelState.AddModelError(string.Empty, "Passwords do not match");
return View();
}
var resetPasswordResult = await _userManager.ResetPasswordAsync(user, token, password);
if (!resetPasswordResult.Succeeded)
{
foreach(var error in resetPasswordResult.Errors)
ModelState.AddModelError(string.Empty, error.Description);
return View();
}
return Content("Password updated");
}
In case you are wondering why didn’t I add the token and user id as hidden fields in the form, it’s because the MVC model binder will find them in the url parameters. Even though most examples (and the default template with Individual User Accounts from Visual Studio) add them as hidden fields, it’s not really necessary.
There’s not much about this action, just use _userManager.ResetPasswordAsync
to set a new password for the user and that’s it.
Logging out
Logging a user out just involves using the SignInManager
and calling SignOutAsync
:
[HttpPost]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return Redirect("~/");
}
It’s considered good practice to only allow the user to logout in response to an HTTP POST request.
Note that I did not do this in the example for simplicity reasons. So if you are following along don’t add the [HttpPost]
(the Index page has a link to /Account/Logout
). Either that, or change the logout link in the Index.cshtml to a form that posts to /Account/Logout
.
Configuration
You might have wondered, will all this work if I choose a different name than AccountController
? Maybe name the login action as SignIn
instead of Login
.
If you do that it won’t work. For example if you use the [Authorize]
attribute in a controller action and the user navigates to the url for that action a redirect will be issued to /Account/Login
.
So how can you change all that? It’s when you register the Identity service in Startup.cs
‘s ConfigureServices
.
Here’s how you can change the default login page:
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddIdentity<IdentityUser, IdentityRole>(options => {
options.Cookies.ApplicationCookie.LoginPath = "/Account/SignIn";
})
//...
Alternatively, you can use the Configure extension method with IdentityOptions as its template parameter:
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<IdentityOptions>(options => {
options.Cookies.ApplicationCookie.LoginPath = "/Account/SignIn";
});
}
The end result is the same.
In case you are wondering, the “AplicationCookie” represents the cookie that is issued by SignInManager
. There are other cookies for external login providers and two factor authentication.
A common configuration task is to setup the password requirements, for example:
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<IdentityOptions>(options => {
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
}
This would setup the password restrictions to simply 6 or more characters.
Another example of what you can do is to require a confirmed email:
services.Configure<IdentityOptions>(options => {
options.SignIn.RequireConfirmedEmail = true;
});
If you do this you won’t have to check the EmailConfirmed
property of IdentityUser
. When you try to sign the user in using the SignInManager
it will fail, and the result will contain a property named IsNotAllowed
set to true.
There are other configuration options available, for example the number of attempts allowed before an account is locked out, and for how long it’s locked out.
Actually send emails with SendGrid
If you made it this far congratulations, here’s a treat.
If you actually want to send emails you can do it using SendGrid. It’s email as a service and they have a free plan so you can try it out.
To use SendGrid first add the NuGet package in you project.json’s dependencies:
"SendGrid.NetCore": "1.0.0-rtm-00002"
Here’s the implementation for IMessageService using SendGrid:
public class SendGridMessageService : IMessageService
{
public async Task Send(string email, string subject, string message)
{
var emailMessage = new SendGrid.SendGridMessage();
emailMessage.AddTo(email);
emailMessage.Subject = subject;
emailMessage.From = new System.Net.Mail.MailAddress("senderEmailAddressHere@senderDomainHere", "info");
emailMessage.Html = message;
emailMessage.Text = message;
var transportWeb = new SendGrid.Web("PUT YOUR SENDGRID KEY HERE");
try{
await transportWeb.DeliverAsync(emailMessage);
}catch(InvalidApiRequestException ex){
System.Diagnostics.Debug.WriteLine(ex.Errors.ToList().Aggregate((allErrors, error) => allErrors += ", " + error));
}
}
}
To try it out update ConfigureService’s registration of IMessageService in Startup.cs:
services.AddTransient<IMessageService, SendGridMessageService>();