When you set out to create a new web application in ASP.NET you have 2 major choices:
- MVC
- Web API
Today we are going to take a look at creating necessary APIs for user authentication.
- ASP.NET Core Identity
- Authentication
- Saving Cookies
- Generating Tokens
- Create Scaffolding for Web API
Follow the MVC Tutorial
There is a fantastic tutorial to setting up ASP.NET MVC Core with Identity Authentication that generates tokens that are then stored in the browsers cookies. The author of the tutorial wrote this specifically for MVC Razor and when I went through it I decided to implement a solution using Web API.
If you are not familiar with the ASP.NET MVC/Web API Architecture I would recommend following the tutorial to give you a beter idea. If not skip ahead and dive right in.
Development Environment
Modified Tutorial
This is a modified tutorial that follows the same methods and teachings as the blog mentioned above. Everything here posted below is my own adaptations from the original blog. I would like to give credit to the original post for showing me everything I needed to do to get this working correctly.
Let’s get started by running a few commands in your terminal
1
2
3
dev@machine:~$ mkdir tutorial
dev@machine:~$ cd ./tutorial
dev@machine:/tutorial$ dotnet new webapi
You will now have a new Web API project located in ~/tutorial
which will generate all the scaffolding needed.
At this point I like to see if everything is working and confirm that I have my environment configured correctly, inside your terminal run the following commands and make sure they complete without errors
1
2
3
dev@machine:~/tutorial$ dotnet restore
dev@machine:~/tutorial$ dotnet build
dev@machine:~/tutorial$ dotnet run
NuGet Packages
Add the following NuGet libraries with the following commands:
1
2
3
4
dev@machine:~/tutorial$ dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dev@machine:~/tutorial$ dotnet add package Microsoft.EntityFrameworkCore.Designer
dev@machine:~/tutorial$ dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dev@machine:~/tutorial$ dotnet add package Microsoft.EntityFrameworkCore.Tools.DotNet
Once you succesfully run all the commands open up your tutorial.csproj file which should look something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
</ItemGroup>
</Project>
There is a problem with the project file above that we need to correct. Line 13 reads
1
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
You will need to update it to the following:
1
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
Configure Identity Core Database
Open up the Startup.cs
file and navigate to the ConfigureServices
method. Modify it to look like the following snippet (we will go over everything in detail in the sub-sections):
Startup:ConfigureServices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void ConfigureServices(IServiceCollection services)
{
// configure database
var server = "localhost";
var connection = $@"Server={server};Database=purchasing_dev;user=sa;password=Password01!;";
services.AddDbContext<IdentityDbContext>(options => options.UseSqlServer(
connection,
optionsBuilder => optionsBuilder.MigrationsAssembly("WebApiIdentityTokenAuth")));
// configure token generation
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();
// configure identity options
services.Configure<IdentityOptions>(o => {
o.SignIn.RequireConfirmedEmail = true;
});
}
Finally in the Configure method we need to specify that we want to use idenity by app.UseIdentity()
Startup:Configure
1
2
3
4
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseIdentity();
}
Configure Database Connection
To configure the databsae all you need is a user that can create databases. Everything else is built right into the Identity libraries. You don’t need to create any special DbContext’s or anything. You should be able to just run the following statements and you are good to go.
- Create connection string
- Configure IdentityDbContext to use connection string
1
2
3
4
5
var server = "localhost";
var connection = $@"Server={server};Database=purchasing_dev;user=sa;password=Password01!;";
services.AddDbContext<IdentityDbContext>(options => options.UseSqlServer(
connection,
optionsBuilder => optionsBuilder.MigrationsAssembly("api")));
Configure Token Generation
This snippet configures our IdentityUser
to use the Token provider. This is a super important step which helps generate our custom cookie that is sent to the client. Without the cookie the user will have to re-authenticate on each call.
1
2
3
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();
Configure Identity Options
We may elect certain password security rules, with Identity it is super easy to do this. Our current policy is enabling email confirmation.
1
2
3
services.Configure<IdentityOptions>(o => {
o.SignIn.RequireConfirmedEmail = true;
});
Enable CORS
Since we are using Web API instead of MVC it will be useful to enable CORS which allows us to use our Web API from other sites than the one that the Web API is hosted on.
Startup:ConfigureServices
1
services.AddCors();
Startup:Configure
1
2
3
4
5
app.UseCors(b => b.WithOrigins("http://dev.localhost.com:4000")
.AllowAnyOrigin()
.AllowCredentials()
.AllowAnyMethod()
.AllowAnyHeader());
Dependency Injection
We touched on confirming emails earlier when the user creates their account. It will be useful to inject a service into our controllows so let’s add some code into the ConfigureServices
method that does just that.
Startup: ConfigureServices
1
services.AddTransient<IMessageService, FileMessageService>();
IMessageService Interface
1
2
3
4
public interface IMessageService
{
Task Send(string email, string subject, string message);
}
We are not going to implement this interface in the tutorial here, but you can view the code or reference the MVC Identity Post mentioned above. This is here for your reference.
Troubleshooting
Please provide your own implementation for these classes or the project will not build.
Create the Database
If you have made it this far and everything builds then you are ready to create your database. Open up the terminal and run the following commands:
1
2
dev@machine:~/tutorial$ dotnet ef migrations add InitialIdentityCreate
dev@machine:~/tutorial$ dotnet ef database update
dotnet ef migrations add InitialIdentityCreate
- This command will create the necessary migration files that will create your database. This is a scaffolding command
dotnet ef database update
- This will run your migration scaffolding against the database that is specifeid in your
Startup.cs
file.
Troubleshooting
If you can’t get these commands to work confirm that you have successfully added Microsoft.EntityFrameworkCore.Tools.DotNet to your tutorial.csproj file.
Build the API
Now that everything is configured let’s build out our API! As before we are going to show the entire class to start and then go through each step in detail.
When I am building a Web API I like to explicitly specify all my routes to make it super clear on how to call the method and retrieve that data. You will see the methods appended by [Route("methodName")]
AccountController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
[Route("api/[controller]")]
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)
{
_userManager = userManager;
_signInManager = signInManager;
_messageService = messageService;
}
[HttpPost]
[Route("register")]
public async Task<JsonResult> Register(string email, string password, string confirmPassword)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "email or password is null"
});
}
if (password != confirmPassword)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Passwords don't match!"
});
}
var newUser = new IdentityUser
{
UserName = email,
Email = email
};
IdentityResult userCreationResult = null;
try
{
userCreationResult = await _userManager.CreateAsync(newUser, password);
}
catch(SqlException)
{
return Json(new Response(HttpStatusCode.InternalServerError)
{
Message = "Error communicating with the database, see logs for more details"
});
}
if (!userCreationResult.Succeeded)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "An error occurred when creating the user, see nested errors",
Errors = userCreationResult.Errors.Select(x => new Response(HttpStatusCode.BadRequest)
{
Message = $"[{x.Code}] {x.Description}"
})
});
}
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 Json(new Response(HttpStatusCode.OK){
Message = $"Registration completed, please verify your email - {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 new RedirectResult("http://dev.localhost.com:4000/registration.html");
}
return new RedirectResult("http://dev.localhost.com:4000/");
}
[HttpPost]
[Route("login")]
public async Task<JsonResult> Login(string email, string password)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "email or password is null"
});
}
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Invalid Login and/or password"
});
}
if (!user.EmailConfirmed)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Email not confirmed, please check your email for confirmation link"
});
}
var passwordSignInResult = await _signInManager.PasswordSignInAsync(user, password, isPersistent: true, lockoutOnFailure: false);
if (!passwordSignInResult.Succeeded)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Invalid Login and/or password"
});
}
return Json(new Response(HttpStatusCode.OK)
{
Message = "Cookie created"
});
}
[HttpPost]
[Route("logout")]
public async Task<JsonResult> Logout()
{
await _signInManager.SignOutAsync();
return Json(new Response(HttpStatusCode.OK)
{
Message = "You have been successfully logged out"
});
}
}
Inject All the Dependencies
Let’s inject everything that we are going to need. When we went through the configuration phase of the tutorial we registered a bunch of dependencies that we are going to be using in the AccountController
1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IMessageService _messageService;
public AccountController(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IMessageService messageService)
{
_userManager = userManager;
_signInManager = signInManager;
_messageService = messageService;
}
Register a New User
The following steps produce the registration process
- Verify we have an email and password
- Confirm the passwords match
- Create the
IdentityUser
and attempt to save it - Check the results object for errors and if they exist return
- Generate an email confirmation token and send it to the provided email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
[HttpPost]
[Route("register")]
public async Task<JsonResult> Register(string email, string password, string confirmPassword)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "email or password is null"
});
}
if (password != confirmPassword)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Passwords don't match!"
});
}
var newUser = new IdentityUser
{
UserName = email,
Email = email
};
IdentityResult userCreationResult = null;
try
{
userCreationResult = await _userManager.CreateAsync(newUser, password);
}
catch(SqlException)
{
return new Response(HttpStatusCode.InternalServerError)
{
Message = "Error communicating with the database, see logs for more details"
};
}
if (!userCreationResult.Succeeded)
{
return new Response(HttpStatusCode.BadRequest)
{
Message = "An error occurred when creating the user, see nested errors",
Errors = userCreationResult.Errors.Select(x => new Response(HttpStatusCode.BadRequest)
{
Message = $"[{x.Code}] {x.Description}"
})
};
}
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 Json(new Response(HttpStatusCode.OK){
Message = $"Registration completed, please verify your email - {email}"
});
}
Verify a New User
When we verify the new user we redirect them either to the login page or the registration page depending on if it is a valid verificaiton token or not.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 new RedirectResult("http://dev.localhost.com:4000/registration.html");
}
return new RedirectResult("http://dev.localhost.com:4000/");
}
Login
The login method is pretty straight forward, we take in the email and password and verify they are a user. Then we call the Identity APIs verifying they are correct which will return an authentication cookie which gets stored as a cookie on the browser.
It is important to note that we aren’t doing anything special to store the cookie this is handled automatically by the Web API and the Identity Library.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[HttpPost]
[Route("login")]
public async Task<JsonResult> Login(string email, string password)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "email or password is null"
});
}
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Invalid Login and/or password"
});
}
if (!user.EmailConfirmed)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Email not confirmed, please check your email for confirmation link"
});
}
var passwordSignInResult = await _signInManager.PasswordSignInAsync(user, password, isPersistent: true, lockoutOnFailure: false);
if (!passwordSignInResult.Succeeded)
{
return Json(new Response(HttpStatusCode.BadRequest)
{
Message = "Invalid Login and/or password"
});
}
return Json(new Response(HttpStatusCode.OK)
{
Message = "Cookie created"
});
}
Logout
The Logout logic is super easy with the Identity library, just call SignOutAsync()
and we are done!
1
2
3
4
5
6
7
8
9
10
11
[HttpPost]
[Route("logout")]
public async Task<JsonResult> Logout()
{
await _signInManager.SignOutAsync();
return Json(new Response(HttpStatusCode.OK)
{
Message = "You have been successfully logged out"
});
}
Test the API
At this point I would recommend using a tool to test your API that is able to receive cookies. Below is a screenshot of what the cookie looks like in postman
Front-End - Javascript with jQuery
There are countless options of how to develop a front end from simple jQuery to Angular to a mobile app. We aren’t going to go through every possible front end option. That is the whole point of creating a Web API being able to swap out the front end with a different implementation.
What we are going to do is talk about jQuery and how to use it with your API. When I went through this on my own I spent several hours trying to figure out what I was doing wrong.
This example may not be the best way to handle your UI, but it is a quick and dirty way to demonstrate using the Web API
Login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$.ajax({
type: 'post',
url: 'http://dev.localhost.com:5000/v1/api/account/login',
data: user,
xhrFields: {
withCredentials: true
}
}).done(function(data){
if(data.code == 200)
{
window.location.href = "http://dev.localhost.com:4000/home.html";
}
else
{
account.$loginErrorCallout.html(data.message);
account.$loginErrorCallout.show();
}
});
Credential Passing
You may be asking yourself, because I did, “Why can’t I just use $.post()
, what is so special about $.ajax()
”. If you do not specify xhrFields.withCredentials: true
none of this will work.
The cookie will be returned like the Web API always does from the login
method but it wont’ be saved. All the methods from here on out that use the cookie will need be setup passing the proper xhrFields.withCredentials
property.
GitHub Example
You did it!! At this point you should have a working Web API with Identity Token Authentication. If you are having trouble just fork my GitHub sample that has all the Web API code from this tutorial.