Introduction
I initially wrote this following how-to as a reminder for me. I gathered its contents from experience and other readings: books and articles. Then I wanted to share it with you hoping that will be helpful to somebody even though there are a lot of articles and how-tos talking about this fabulous subject.
Let's start at the beginning.
When you start a web application, you will be prompted by a screen showing options of authorization. I will not cover those options because the one that interests me here is individual user account.
You also get a change button and by default the individual user authorization is selected. Leave it as is and go ahead and create your MVC application.
Once your application created, you've got out-of-the-box many things regarding authentication and authorization. To get a feeling of that, click on register link and get yourself registered. Then the application will recognize you by displaying your name and greeting you. You can click on that and you will be redirected to manage account view to be able to change your password. You could also logoff and login again. All the work behind the scene is done for you.
The links we just used are in the _loginPartial
view which is located in shared folder. Those links trigger actions on AccountController
.
Of course, if the application remembers you, it's because there is somewhere a persistence mechanism and a repository to store the user information. We will get there later.
Now, if you want to restrict access to authenticated user to any action of a controller or to a complete controller, you add [Authorize]
attribute to it. When you try to get to it, if you are not logged in you will be redirected to login view. The login URL is configured in StartupAuth.cs that you can find in App_Start folder. Here is the snippet:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
And of course, you may restrict access to all users except for those you can specify as follows:
[Authorize(Users="m2a")]
or by role or many roles [Authorize(Role="admin, techees")]
Even if you add those attributes on controller level, you may allow access to any action by adding a less restrictive role or attribute as [AllowAnonymous]
. It's pretty simple stuff that you can try easily.
Database
As I said before, behind the scenes, there is a database where the user information is stored. Click on your project and Show All Files. Under the App-Data folder, you will get an MDF extension file which is the database.
Double click on it and it will be opened in the server explorer if you want to explore it. But the user information as hashedpassword is stored in the AppNetUser
table.
One thing I don't like about this is the database name. It's maybe just a detail but I want the name to be more significant than this aspnet-thing+datetime... So to do that, open your Web config file and go to connectionstring
section. You will see that by default the connection string is DefaultConnection
. So change the name there and the catalog name also to something that makes sense to you and rebuild the application. You will notice that a new database is created and you have to register again. To delete the old database, you need to do it from file server and in server explorer.
Core Identity
The AspNet.Identity.Core
is the API that does the magic behind the scenes. It exposes many interfaces (repository pattern implementation) as IUser
, IRole
, IUserStore
, etc. There also many concrete objects as UserManager
and RoleManager
. We will get introduced to UserManager
class later, but basically it exposes the domain logic to manage the user information as hashing the password. There is also a UserStore
that could be used to manage a user but you should always use UserManager
unless you need something low level and need UserStore
to do it. For example, if you need to control how the user information is persisted or where it's persisted, you may want to use MongoDb or any other NoSql database of your choice to persist the users. If you choose to stick to SQL Server, then entityframework comes into the play.
Entity Framework
In the Identity.EntityFramework
assembly, you have many objects that facilitate the user and role management as IdentityUser
and IdentityRole
. You have also IdentityDbContext
that allows you to interact with the SQL database.
Extending Identity
Let's say you want to add a secret question to your login view to be able to record it as security measure. To do just that, open identityModel
class and add a property to ApplicationUser
class as shown here:
public class ApplicationUser : IdentityUser
{
public string SecretQuestion { get; set; }
public string SecretQuestionResponse { get; set; }
}
Now you need to hook this up to the login view to be able to get that from the user. Now we realize that we need also some useful information about the company and we go ahead and define an agency poco as:
public class Agency
{
public int Id { get; set; }
public string Name { get; set; }
public int NumberOfEmployees { get; set; }
}
To get this poco in interaction with Entity Framework, we need a DbSet
class referring to this object. We add this as follows:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection")
{
}
public DbSet<Agency> AgencyContext;
}
The ApplicationDbContext
gets the IdentityDbContext
class which gives access to user and role management and also AgencyContext
. This means everywhere you instantiate this object, you will get full control over them.
But remember that as far as user is concerned, UserManager
is a better option as we stated earlier. Again, it depends on what you are trying to do.
Now, we are set up and we run the application. The yellow screen of death shows up. Look at figure 1 to see how it looks like if you missed it. But, basically, it's saying you need to update your database to take care of the new things you just added.
To do that, we need to enable-migrations using Package Manager Console. Once this is added, a Migrations folder is added to solution and a configuration class inside it.
In the configuration class, set automaticMigrationEnabled
to true
to avoid keeping scripts while you are in the development process. You still need to manage those scripts once you feel ready to baseline your database before going to production.
internal sealed class Configuration : DbMigrationsConfiguration<identity2.Models.ApplicationDbContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
ContextKey = "identity2.Models.ApplicationDbContext";
}
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
}
}
Notice there the seed method. This one is to seed the database with reference data. May be the users, roles or any data that you want to be there right after database creation.
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
context.Users.AddOrUpdate(firstUser => firstUser.UserName, new ApplicationUser { UserName = "Mack", PasswordHash = new PasswordHasher().HashPassword("password") });
}
This way of doing things is too much EntityFrameworkish to me. Since I have an example in AccountController
how to manage this, I will refactor it as follows:
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
if (!context.Users.Any(user => string.Compare(user.UserName, "mack", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var userstore = new UserStore<ApplicationUser>(context);
var usermanager = new UserManager<ApplicationUser>(userstore);
usermanager.Create(new ApplicationUser {UserName = "mack"}, "password");
}
}
If you take a look at the constructor of AccountController
, you will notice that UserStore
needs a context and UserManager
needs a UserStore
:
public AccountController()
: this(new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
}
We did just that in the seed method and we added the user and password to the create function. Now go ahead and update-database via the Manager Console.
If you want to see what's going on when you have pending migrations to apply add -verbose flag after update-database. After that, we run again the application and everything is up and running as expected.
Here is the updated code to include the 'admin' role:
protected override void Seed(identity2.Models.ApplicationDbContext context)
{
if (!context.Roles.Any(role => string.Compare(role.Name, "admin", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var rolestore = new RoleStore<IdentityRole>(context);
var rolemanager = new RoleManager<IdentityRole>(rolestore);
rolemanager.Create(new IdentityRole { Name = "admin" });
}
if (!context.Users.Any(user => string.Compare(user.UserName, "mack", StringComparison.CurrentCultureIgnoreCase) == 0))
{
var userstore = new UserStore<ApplicationUser>(context);
var usermanager = new UserManager<ApplicationUser>(userstore);
var user = new ApplicationUser { UserName = "Mack" };
usermanager.Create(user, "password");
usermanager.AddToRole(user.Id, "admin");
}
}
As you notice, there is room for refactoring the seed method. Because if you have more than 100 users to add, you don't want to repeat the same code more than 100 times.
External Logins
External logins use protocol as OpenId or OAuth to provide a user identity. You register the application in the provider site and you get back a key to add to your application.
How the identity provider provides you the user identity is a little bit foggy for me.
It's complex enough to avoid it here and for sure I don't understand the 'perform discovery' actions and Google’s xrds document...Anyway, there is a lot of stuff out there if you feel in shape to learn more about it.
Still, the advantage of giving this opportunity to your users is that they do not have to memorize another password and you as developer the big advantage lays in the fact that you don't have to worry about storing and managing user's credentials, etc. But you still have to trust providers such as Microsoft, Google, Facebook, Twitter, etc.
To enable that, uncomment the code in Startup object (in Startup.Auth.cs file). Note that Google does not require any key or registration. Uncomment the code as follows:
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication();
}
And click on Google button on the login view. Follow instruction and finally you be redirected as a recognised user in the application. Note that you could even change the name you display after the famous Hello greeting!
Behind the scene OWIN is doing the magic. To get a glimpse on that, browse back to AccountController
and look at AuthenticationManager
property. It's coming from Owin Context.
To see the email address, you used to login to Google for example, go to ExternalLoginCallback
function and write the following at the beginning:
var fromGoogle= await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie);
You can now browse to Identity and then to EmailAddress Claim to get the email address.