Asp.NET Core 2.0 WebApi and Identity – Customizing Identity Models and Implementing Role-Based Authorization
Consider Your Use Case Before Deciding on Your Auth Strategy
There are a number of options for Authentication and Authorization strategies in a Web Api project. Use of Bearer tokens and Role-Based Authentication is relatively simple to implement, but is not the most advanced architecture for an authorization solution. Before deciding upon traditional Role-Based Authorization, you may want to examine the scope of your authentication and authorization needs, and determine if something simpler, or something more advanced, may be warranted.
ASP.NET Web Api can take full advantage of Claims-Based Authorization, which, for more complex systems, may be a better choice. Similarly, as mentioned previously, if the primary purpose of your Web Api is to act as an Authentication Service, you may want to go with a more robust token system (for example, shared private keys as opposed to the bearer tokens used by default), and do away with authorization at this level.
Role-Based Authorization is a good fit in a project where there exists a modest need for different levels of authorization/access, and possibly the Web Api is a part of, or associated with, a larger MVC or other ASP.NET site where Roles are used to govern authorization. Consider a standard MVC project, in which a few roles are sufficient to manage authorization, and which serves web pages as well as offers API access.
Getting Started – Create a New ASP.NET Web Api Project
First, in Visual Studio, create a new ASP.NET Web Api project. Once the project is created, update the Nuget packages in the solution, either using Manage Packages for Solution in the context menu for Solution Explorer, or by using Update-Package
in the Package Manager Console.
Adding a Role Model, and Customizing ApplicationUser
To get started, let’s take another look at the Models => IdentityModes.cs file. Currently, there is not a lot there:
The Default IdentityModels.cs File in Web Api:
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
namespace AspNetIdentity2WebApiCustomize.Models
{
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
UserManager<ApplicationUser> manager, string authenticationType)
{
// Note the authenticationType must match the one defined in
// CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
// Add custom user claims here
return userIdentity;
}
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
}
As we did in our ASP.NET MVC examples, we begin by modifying and adding to the existing models defined in Models => IdentityModels.cs. In fact, since we did a lot of the work previously, we will start by stealing the IdentityModels.cs code from the ASP.NET Extensible Template Project. Careful here. We can save ourselves some pain by pasting the classes into the existing namespace defined in the current code file, and leaving the using statements as they are for the moment:
Updated IdentityModels.cs Code:
// You will not likely need to customize there, but it is necessary/easier to create our own
// project-specific implementations, so here they are:
public class ApplicationUserLogin : IdentityUserLogin<string> { }
public class ApplicationUserClaim : IdentityUserClaim<string> { }
public class ApplicationUserRole : IdentityUserRole<string> { }
// Must be expressed in terms of our custom Role and other types:
public class ApplicationUser
: IdentityUser<string, ApplicationUserLogin,
ApplicationUserRole, ApplicationUserClaim>
{
public ApplicationUser()
{
this.Id = Guid.NewGuid().ToString();
// Add any custom User properties/code here
}
public async Task<ClaimsIdentity>
GenerateUserIdentityAsync(ApplicationUserManager manager)
{
var userIdentity = await manager
.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
public ApplicationRole()
{
this.Id = Guid.NewGuid().ToString();
}
public ApplicationRole(string name)
: this()
{
this.Name = name;
}
// Add any custom Role properties/code here
}
// Must be expressed in terms of our custom types:
public class ApplicationDbContext
: IdentityDbContext<ApplicationUser, ApplicationRole,
string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
static ApplicationDbContext()
{
Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
// Add additional items here as needed
}
// Most likely won't need to customize these either, but they were needed because we implemented
// custom versions of all the other types:
public class ApplicationUserStore
:UserStore<ApplicationUser, ApplicationRole, string,
ApplicationUserLogin, ApplicationUserRole,
ApplicationUserClaim>, IUserStore<ApplicationUser, string>,
IDisposable
{
public ApplicationUserStore()
: this(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationUserStore(DbContext context)
: base(context)
{
}
}
public class ApplicationRoleStore
: RoleStore<ApplicationRole, string, ApplicationUserRole>,
IQueryableRoleStore<ApplicationRole, string>,
IRoleStore<ApplicationRole, string>, IDisposable
{
public ApplicationRoleStore()
: base(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationRoleStore(DbContext context)
: base(context)
{
}
}
Now, we need to add a few additional using statements at the top of the code file to bring in some references we need with the new code. Add the following to the using statements at the top of the file:
Additional Using Statements Added to IdentityModels.cs:
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using System;
There are two immediate problems with the code we just pasted in there. The first is probably obvious, because the VS compiler is probably telling you that there is no DBInitializer
class defined. Also, if you Build the project, a few other problems will surface in the VS error list. We’ll take care of that in a moment.
The other is not so obvious. The code we pasted in here is from an MVC project. For the most part this is fine. However, our ApplicationUser
class defines a method GenerateUserIdentityAsync
. The code we stole from our MVC project has this method, but defines it in terms of a single parameter of type ApplicationUserManager
. Recall the code we pasted over, which defined ApplicationUser
with two constructor parameters. The missing parameter in our newly copied method is of type string
, and represents the authenticationType
.
This is important, because GenerateUserIdentityAsync
is called when we need to retrieve a user’s ClaimsIdentity
, which represents the various claims the specific user has within our system.
Confused yet? We don’t need to worry about the details of ClaimsIdentity
just yet. What we DO need to do is update the GenerateUserIdentityAsync
method defined on ApplicationUser
to accept a string
parameter representing the authenticationType
.
Update ApplicationUser for Web Api
To make our ApplicationUser
class ready for use in a Web Api context, we can replace the code for the GenerateUserIdentityAsync
method with the following:
Update GenerateUserIdentityAsync with Authentication Type Parameter:
// ** Add authenticationtype as method parameter:
public async Task<ClaimsIdentity>
GenerateUserIdentityAsync(ApplicationUserManager manager, string authenticationType)
{
// Note the authenticationType must match the one defined
// in CookieAuthenticationOptions.AuthenticationType
var userIdentity =
await manager.CreateIdentityAsync(this, authenticationType);
// Add custom user claims here
return userIdentity;
}
Adding a DBInitializer and Other Identity Config Items
We mentioned earlier, and the compiler is helpfully pointing out to you, that the code we stole from the Identity Extensible Template project is attempting to use a DBInitializer
class that doesn’t exist (yet) in our Web Api project. Also, you probably notice (if you have built the project since adding the additional Identity Models), that there appear to be some problems with our new ApplicationUser
class.
We will resolve most of these issues by once again stealing select bits of code from the Identity Extensible Template project.
If we look at the App_Start => IdentityConfig.cs file in our Web Api project, we see that, as with the original IdentityModels.cs file, there is not much there:
The Default Identity.config File from a Web Api Project:
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using AspNetIdentity2WebApiCustomize.Models;
namespace AspNetIdentity2WebApiCustomize
{
// Configure the application user manager used in this application. UserManager
// is defined in ASP.NET Identity and is used by the application.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager =
new ApplicationUserManager(
new UserStore<ApplicationUser>(
context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
}
In order to work with Roles in our Web Api project, we will need an ApplicationRoleManager
, and as mentioned previously, we will be adding the ApplicationDbInitializer
from the Extensible Template project.
First, we need the following using statements at the top of the IdentityConfig.cs file:
Using Statements for the IdentityConfig.cs File:
using AspNetIdentity2WebApiCustomize.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System.Data.Entity;
using System.Web;
Now, add the ApplicationRoleManager
and ApplicationDbInitializer
classes from the Extensible Template project to our IdentityConfig.cs file:
Add ApplicationRoleManager and ApplicationDbInitializer to IdentityConfig.cs:
using AspNetIdentity2WebApiCustomize.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System.Data.Entity;
using System.Web;
namespace AspNetIdentity2WebApiCustomize
{
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager = new ApplicationUserManager(
new UserStore<ApplicationUser>(
context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
public ApplicationRoleManager(IRoleStore<ApplicationRole, string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options,
IOwinContext context)
{
return new ApplicationRoleManager(
new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
}
}
public class ApplicationDbInitializer
: DropCreateDatabaseAlways<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
InitializeIdentityForEF(context);
base.Seed(context);
}
//Create User=Admin@Admin.com with password=Admin@123456 in the Admin role
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
var userManager = HttpContext.Current
.GetOwinContext().GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext().Get<ApplicationRoleManager>();
const string name = "admin@example.com";
const string password = "Admin@123456";
const string roleName = "Admin";
//Create Role Admin if it does not exist
var role = roleManager.FindByName(roleName);
if (role == null)
{
role = new ApplicationRole(roleName);
var roleresult = roleManager.Create(role);
}
var user = userManager.FindByName(name);
if (user == null)
{
user = new ApplicationUser { UserName = name, Email = name };
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name))
{
var result = userManager.AddToRole(user.Id, role.Name);
}
}
}
}
Now, we need to make a few changes to our ApplicationUserManager
. Since we have added our customizable models, including modified versions of UserStore
and RoleStore
, we need to adapt the ApplicationUserManager
to play nice. We have expressed our models with different type arguments than the default implementation expected by the Web Api project. Specifically, we have employed a customized implementation of IUserStore
. Rather than the concrete UserStore
defined in Microsoft.AspNet.Identity.EntityFramework
, we have implemented our own ApplicationUserStore
, which is expressed in terms of specific type arguments.
We now need to tune up our ApplicationUserManager
to work with our ApplicationUserStore
.
Change the code for ApplicationUserManager
in IdentityConfig.cs to the following:
Modified ApplicationUserManager:
public class ApplicationUserManager
: UserManager<ApplicationUser, string>
{
public ApplicationUserManager(IUserStore<ApplicationUser, string> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager = new ApplicationUserManager(
new UserStore<ApplicationUser, ApplicationRole, string,
ApplicationUserLogin, ApplicationUserRole,
ApplicationUserClaim>(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
With that, the very minimal basics are in place for us to use our new, extensible model classes (including Roles, which were not directly available to us in the default Web Api implementation) in our Web Api project.
We do need to clean up one more issue though. The AccountController
still appears to rely on Microsoft.AspNet.Identity.EntityFramework.IdentityUser
, and we need it to use our new implementation ApplicationUser
.
Modify AccountController to Use ApplicationUser
We can easily correct this last remaining issue. Open the AccountController
class, and locate the GetManageInfo()
method. We can see where a local variable user
is declared, explicitly types as IdentityUser
.
Below that, we can see in the foreach()
loop, we explicitly declare an iterator variable linkedAccount
as type IdentityUserLogin
.
Existing Code in the Web Api AccountController GetManageInfo() Method:
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(
string returnUrl, bool generateState = false)
{
IdentityUser user =
await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (IdentityUserLogin linkedAccount in user.Logins)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
In both cases we have implemented our own versions of these types. Here, we can either change the declaration in each case to use the var
keyword, which relieves us of the type constraint on the variable (but, some would argue, makes our code a bit ambiguous), or we can change the explicit type declaration in each case to use our own implementation.
For now, let’s change the explicit type declaration to use our own implementations:
Modified Code for GetManageInfo() Method:
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(
string returnUrl, bool generateState = false)
{
ApplicationUser user =
await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (ApplicationUserLogin linkedAccount in user.Logins)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
Above, we have simply changed the declared type for the local user variable from IdentityUser
to ApplicationUser
, and the iterator variable linkedAccount
from IdentityUserLogin
to ApplicationUserLogin
.
Add Initialization for ApplicationRoleManager in Startup.Auth
Recall from our high-level exploration of ASP.NET Web Api and Identity that initialization and configuration of Identity occurs in the Startup
class defined in App_Start => Startup.Auth.
As we have seen, the original VS Web Api template did not really provide for Role-Based anything, and consequently, provides no configuration or initialization for our recently added ApplicationRoleManager at startup. We need to add a line of initialization code to our Startup.Auth file:
Add Initialization for ApplicationRoleManager in Statup.Auth:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
// ... Code for third-part logins omitted for brevity ...
}
We’ve added a single line, which initializes an instance of ApplicationRoleManager for each incoming request.
About the ApplicationDbInitializer
With the changes we’ve introduced so far, we should be able to take our new and improved Web Api project for a test spin to see if the most basic functionality works.
Before we do, though, we need to recognize that we have fundamentally changed how the EF/Code-First database generation has been changed with the introduction of our custom ApplicationDbInitializer
.
Recall from our explorations of customizing an MVC project with extensible models, the ApplicationDbInitializer
allows us to specify some options for how and when the database behind our application is generated, and to provide some initial data to work with.
As we move towards Role-Based Authorization and a more restrictive security model for our Api, this becomes important.
The way we currently have ApplicationDbInitializer
configured, it derives from DbDropCreateDatabaseAlways
, which means every time we run our application, the backing store will be destroyed, and re-created from scratch. We also have it set up to create a default User, and we assign that user to the Admin role. In this manner, we start our application with a user with Admin-level access permissions.
The default VS Web Api project doesn’t take advantage of this out of the box. If we look at the class declaration for AccountController
we see that the class itself is decorated with a simple [Authorize]
attribute. What this essentially does is restrict access to all of the Action methods on the class to authorized users (except those methods specifically decorated with an [AllowAnonymous]
attribute).
In other words, for now, any user who is registered, and who successfully signs in and presents a valid Bearer Token can access any of the Action methods on AccountController
.
We’ll take a closer look at implementing Role-Based Authentication momentarily, First, we will extend our ApplicationUser and ApplicationRole classes with some custom properties.
Adding Custom Properties to ApplicationUser and ApplicationRole
As we saw when we examine customizing Users and Roles within an MVC project, we will add a few simple properties to our ApplicationUser and ApplicationRole models. Modify the code for each as follows:
Add Custom Properties to ApplicationUser and ApplicationRole:
// Must be expressed in terms of our custom Role and other types:
public class ApplicationUser
: IdentityUser<string, ApplicationUserLogin,
ApplicationUserRole, ApplicationUserClaim>
{
public ApplicationUser()
{
this.Id = Guid.NewGuid().ToString();
}
public async Task<ClaimsIdentity>GenerateUserIdentityAsync(
ApplicationUserManager manager, string authenticationType)
{
// Note the authenticationType must match the one
// defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
// Add custom user claims here
return userIdentity;
}
// Add Custom Properties:
public string Address { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
}
// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
public ApplicationRole()
{
this.Id = Guid.NewGuid().ToString();
}
public ApplicationRole(string name) : this()
{
this.Name = name;
}
// Add Custom Property:
public string Description { get; set; }
}
Here, we have added an Address
and related properties to ApplicationUser
, and a simple Description
property to ApplicationRole
.
Now, let’s update our ApplicationDbInitializer
to set some sample values for these new properties. Update the code for the InitializeIdentityForEF()
method as follows:
Set Initial Values for Custom Properties in ApplicationDbInitializer:
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
var userManager = HttpContext.Current
.GetOwinContext().GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext().Get<ApplicationRoleManager>();
const string name = "admin@example.com";
const string password = "Admin@123456";
// Some initial values for custom properties:
const string address = "1234 Sesame Street";
const string city = "Portland";
const string state = "OR";
const string postalCode = "97209";
const string roleName = "Admin";
const string roleDescription = "All access pass";
//Create Role Admin if it does not exist
var role = roleManager.FindByName(roleName);
if (role == null)
{
role = new ApplicationRole(roleName);
// Set the new custom property:
role.Description = roleDescription;
var roleresult = roleManager.Create(role);
}
var user = userManager.FindByName(name);
if (user == null)
{
user = new ApplicationUser { UserName = name, Email = name };
// Set the new custom properties:
user.Address = address;
user.City = city;
user.State = state;
user.PostalCode = postalCode;
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name))
{
var result = userManager.AddToRole(user.Id, role.Name);
}
}
With that, we should be ready to see if everything at least works correctly . . .
Create a Simple Web Api Client Application
To see if everything is working properly to this point, we will create a simple console application as an Api client.
In Visual Studio, create a new Console Application, and the use the Manage Nuget Packages for Solutions to add the Microsoft Asp.NET Web Api Client Libraries, or use the Package Manager Console and do:
Add Web Api 2.2 via the Nuget Package Manager Console:
PM> Install-Package Microsoft.AspNet.WebApi.Client
Now that we have the required Web Api Client Libraries in our project, open the Program.cs file.
Make sure the following using statements are present at the top of the file. Note we have added references to System.Net.Http
and Newtonsoft.Json
, ad well as System.Threading
:
Required Using Statements for Console Api Client Application:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using Newtonsoft.Json;
Next, let’s add some very basic client code used to retrieve a token from the token endpoint of our Web Api. Add the following within the Program class:
Add Client Code to Retreive Response from Web Api Token Endpoint:
// You will need to substitute your own host Url here:
static string host = "http://localhost:63074/";
static void Main(string[] args)
{
Console.WriteLine("Attempting to Log in with default admin user");
// Get hold of a Dictionary representing the JSON in the response Body:
var responseDictionary =
GetResponseAsDictionary("admin@example.com", "Admin@123456");
foreach(var kvp in responseDictionary)
{
Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
}
Console.Read();
}
static Dictionary<string, string> GetResponseAsDictionary(
string userName, string password)
{
HttpClient client = new HttpClient();
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>( "grant_type", "password" ),
new KeyValuePair<string, string>( "username", userName ),
new KeyValuePair<string, string> ( "Password", password )
};
var content = new FormUrlEncodedContent(pairs);
// Attempt to get a token from the token endpoint of the Web Api host:
HttpResponseMessage response =
client.PostAsync(host + "Token", content).Result;
var result = response.Content.ReadAsStringAsync().Result;
// De-Serialize into a dictionary and return:
Dictionary<string, string> tokenDictionary =
JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
return tokenDictionary;
}
Note, at this point, we have added code the the Main() method, which calls out to a single, rather contrived method GetResponseAsDictionary()
. All we are basically doing here is submitting an HTTP POST to the Token endpoint of our Web Api, and then de-serializing the JSON response body into a Dictionary<string, string>
.
Once we have the Dictionary, we are iterating over each Key/Value pair, and writing the contents to the Console.
If everything has worked the way we expect, our Console output should be something along the lines of the following:
Console Output from Token Endpoint Response:
Attempting to Log in with default admin user
access_token: AuzOQkgG3BYubP1rlljcPhAzW7R7gA4Vew8dHy_MScMn2-Rs3R6dNlwCU_SuFwKveq
uf5rflB7PCfamlcT_-KJ4q3lfx7kiFNpSF9SdMLwKP_mCSOXGbrxrK3jXfH7bum3sZdl7w8k5irLa27i
Bvp_RqtXgkSmgpcNWitCU8RBz7aOaHr8r-FCklg4wUkLNE26qlR6Sl42DAAiBZNLpUZUt-M7vaOs8TZB
W4YehAzrqFAuTX3peMJBQB8K8_XxaTkRnEhSEMz9DnUnqzQjjVr5rnSdFSGxQmrQA8dBBwq4RaUfwbCU
7au787CMn7EGiDO9KRcGHAsGHOJqb8P8Z7A-ssV7tfEqJayrNH-F_Z2p5kiasDODQrG53CZNUE0vuDT6
Fp4_xOavE6wkYcHTfXWZJWFEMokE4NB9mtAl3lReYSZQyzKkcHWFNQCMAj3LoNGSdnEVVM_jzZtRSfWj
IG2OmhyR1wZNRCHY_6NwEMOIHGLpA_L-kFFAJPgwQWi-WljeV-X2KiMQIeYlGGdskaNw
token_type: bearer
expires_in: 1209599
userName: admin@example.com
.issued: Sun, 26 Oct 2014 13:21:03 GMT
.expires: Sun, 09 Nov 2014 13:21:03 GMT
We’ve seen this before, in our overview article. The de-serialized JSON above represents the content of the response to our POST to the Token endpoint of our Web Api. The important part of the response is and access_token
itself.
This Doesn’t Look Any Different Than Before . . .
At this point, that de-serialized JSON response doesn’t look any different than it did in our previous post, before we added all our fancy new Roles and custom properties. Shouldn’t it have some new information in it now? Roles, and Addresses and stuff?
No.
A Little More On the Nature of Bearer Tokens (but only a little)
We had a really, really brief look Bearer Tokens in our Introduction to Identity in Web Api. As mentioned there, we will undertake a more in-depth exploration of Tokens in another post.
For our purposes here today, we are simply going to expand a little on what we learned previously, sufficient to understand how the access_token
we retrieved as part of our JSON response above fits into the scheme of our newly modified Web Api project.
Bearer Tokens are, by design, “opaque” to the client. In other words, they are encoded (and sometimes encrypted) on the server, and can be decoded (and potentially decrypted) by the server. They are not designed to be decoded/decrypted by the client.
Recall from exploring the basic structure of an ASP.NET Web Api project, the ApplicationOauthProvider class, defined in the Providers => ApplicationOauthProvider.cs file.
When you POST a request to the Token endpoint of the ASP.NET Web Api application (at least, in the way we have it configured here), the server validates the credentials you present (in this case, user name + Password) by calling the GrantResourceOwnersCredentials() method defined on the ApplicationOauthProvider class:
The GrantResourceOwnersCredentials() Method from ApplicationOAuthProvider:
public override async Task GrantResourceOwnerCredentials(
OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
We can see that this code attempts to find a user with credentials matching those presented. If a valid user is found, the method calls the GenerateUserIdentityAsync()
method defined on the ApplicationUser
class to obtain an instance of ClaimsIdentity
representing the user, and any claims the user may make within our system.
The ClaimsIdentity
is then used to create an AuthenticationTicket
.
In the code for GrantResourceOwnersCredentials(),
above, when Validated()
is called on the OAuthGrantResourceOwnerCredentialsContext
, the OWIN middleware serializes the ClaimsIdentity
into an encoded and signed token, to be returned in the HTTP Response header as a result of our request.
All of the above is a long-winded way of saying, the important user information, including user roles, and anything else we decide needs to be a part of our token, is present in the token when it is received by the client.
The Client just can’t get to it.
A Note About Bearer Tokens and Security
As mentioned in our previous posts, using bearer tokens, and submitting credentials to obtain the token from the token endpoint should only be done over SSL/STL in a production system. Bearer tokens are exactly what their name implies – anyone presenting a valid bearer token will be granted access to the system, with whatever privileges the actual “owner” of the token has.
OAuth Bearer tokens represent a fairly simple authentication/authorization scheme, In building out your Web Api, consider all of the alternatives, and make the best choice for your application.
As mentioned previously, we will take a more thorough look at Claims Identity, and token authentication/authorization in later posts.
Adding Role-Based Authorization to the Web Api Application
Now that we understand how to authenticate ourselves using a bearer token, let’s look at how we can use our new Role-Based Authorization capability within our application.
To start, let’s look at the two primary controllers present in the Web Api application, AccountController
and ValuesController
(we are ignoring the HomeController
, since it serves no purpose for our needs here).
We’ve already seen that AccountController
is decorated with an [Authorize]
attribute. This means that only authenticated users may access the action methods defined on this controller (unless, of course, the method itself is decorated with an [AllowAnonymous]
attribute).
Let’s look at the simplistic ValuesController
. ValuesController
is provided as a simple example of how one might add a basic CRUD-style functionality. ValuesController
is similarly decorated with the [Authorize]
attribute. Again, only authenticated users are able to access the Action methods defined on ValuesController
.
As we saw in previous posts about implementing Role-Based Authorization in an MVC project, we can modify the access permissions for our Web Api, at either the Controller level, or the Action method level, by expanding on our use of [Authorize]
.
Consider, we might want to restrict access to AccountController only to users who are in the Admin role, but allow access to ValuesController
, and the functionality it provides, to any authenticated user.
In this case, we will want to make some modifications to our Web Api configuration.
Add a Vanilla Users Role as a Default in ApplicationDbInitializer
First, let’s make sure we have two distinct Roles available in our application – the “Admin” role we already create as an initial value during configuration, and a new “Users” role. Update the InitializeDatabaseForEF()
method as follows:
Add a Users Role and a Default User to InitializeDatabaseForEF() Method:
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
var userManager = HttpContext.Current
.GetOwinContext().GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext().Get<ApplicationRoleManager>();
// Initial Admin user:
const string name = "admin@example.com";
const string password = "Admin@123456";
// Some initial values for custom properties:
const string address = "1234 Sesame Street";
const string city = "Portland";
const string state = "OR";
const string postalCode = "97209";
const string roleName = "Admin";
const string roleDescription = "All access pass";
//Create Role Admin if it does not exist
var role = roleManager.FindByName(roleName);
if (role == null)
{
role = new ApplicationRole(roleName);
// Set the new custom property:
role.Description = roleDescription;
var roleresult = roleManager.Create(role);
}
// Create Admin User:
var user = userManager.FindByName(name);
if (user == null)
{
user = new ApplicationUser { UserName = name, Email = name };
// Set the new custom properties:
user.Address = address;
user.City = city;
user.State = state;
user.PostalCode = postalCode;
var result = userManager.Create(user, password);
result = userManager.SetLockoutEnabled(user.Id, false);
}
// Add user admin to Role Admin if not already added
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name))
{
userManager.AddToRole(user.Id, role.Name);
}
// Initial Vanilla User:
const string vanillaUserName = "vanillaUser@example.com";
const string vanillaUserPassword = "Vanilla@123456";
// Add a plain vannilla Users Role:
const string usersRoleName = "Users";
const string usersRoleDescription = "Plain vanilla User";
//Create Role Users if it does not exist
var usersRole = roleManager.FindByName(usersRoleName);
if (usersRole == null)
{
usersRole = new ApplicationRole(usersRoleName);
// Set the new custom property:
usersRole.Description = usersRoleDescription;
var userRoleresult = roleManager.Create(usersRole);
}
// Create Vanilla User:
var vanillaUser = userManager.FindByName(vanillaUserName);
if (vanillaUser == null)
{
vanillaUser = new ApplicationUser
{
UserName = vanillaUserName,
Email = vanillaUserName
};
// Set the new custom properties:
vanillaUser.Address = address;
vanillaUser.City = city;
vanillaUser.State = state;
vanillaUser.PostalCode = postalCode;
var result = userManager.Create(vanillaUser, vanillaUserPassword);
result = userManager.SetLockoutEnabled(vanillaUser.Id, false);
}
// Add vanilla user to Role Users if not already added
var rolesForVanillaUser = userManager.GetRoles(vanillaUser.Id);
if (!rolesForVanillaUser.Contains(usersRole.Name))
{
userManager.AddToRole(vanillaUser.Id, usersRole.Name);
}
}
Above, we have added a new role “Users” and another initial sample user.
Next, let’s modify the [Authorize]
attribute on our AccountController
class, and add a Role argument:
Modified [Authorize] Attribute for AccountController:
[Authorize(Roles= "Admin")]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
// ... All the Code ...
}
Now, we just need to change up our client code to attempt to access some methods from each of the two controllers to see how our Role-Based Authorization is working for us.
Modify Client Code to Attempt Controller Access
Here, we will simply set up some client code to attempt to retreive some basic data from both AccountController
and ValuesController
. We will do this as a user in the Admin Role, and then also as a user in the Users Role.
Change the code in your Console application to match the following:
Modified Client Code to Access Both Controllers with Different Roles:
class Program
{
// You will need to substitute your own host Url here:
static string host = "http://localhost:63074/";
static void Main(string[] args)
{
// Use the User Names/Emails and Passwords we set up in IdentityConfig:
string adminUserName = "admin@example.com";
string adminUserPassword = "Admin@123456";
string vanillaUserName = "vanillaUser@example.com";
string vanillaUserPassword = "Vanilla@123456";
// Use the new GetToken method to get a token for each user:
string adminUserToken = GetToken(adminUserName, adminUserPassword);
string vaniallaUserToken = GetToken(vanillaUserName, vanillaUserPassword);
// Try to get some data as an Admin:
Console.WriteLine("Attempting to get User info as Admin User");
string adminUserInfoResult = GetUserInfo(adminUserToken);
Console.WriteLine("Admin User Info Result: {0}", adminUserInfoResult);
Console.WriteLine("");
Console.WriteLine("Attempting to get Values info as Admin User");
string adminValuesInfoResult = GetValues(adminUserToken);
Console.WriteLine("Admin Values Info Result: {0}", adminValuesInfoResult);
Console.WriteLine("");
// Try to get some data as a plain old user:
Console.WriteLine("Attempting to get User info as Vanilla User");
string vanillaUserInfoResult = GetUserInfo(vaniallaUserToken);
Console.WriteLine("Vanilla User Info Result: {0}", vanillaUserInfoResult);
Console.WriteLine("");
Console.WriteLine("Attempting to get Values info as Vanilla User");
string vanillaValuesInfoResult = GetValues(vaniallaUserToken);
Console.WriteLine("Vanilla Values Info Result: {0}", vanillaValuesInfoResult);
Console.WriteLine("");
Console.Read();
}
static string GetToken(string userName, string password)
{
HttpClient client = new HttpClient();
var pairs = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>( "grant_type", "password" ),
new KeyValuePair<string, string>( "username", userName ),
new KeyValuePair<string, string> ( "Password", password )
};
var content = new FormUrlEncodedContent(pairs);
// Attempt to get a token from the token endpoint of the Web Api host:
HttpResponseMessage response =
client.PostAsync(host + "Token", content).Result;
var result = response.Content.ReadAsStringAsync().Result;
// De-Serialize into a dictionary and return:
Dictionary<string, string> tokenDictionary =
JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
return tokenDictionary["access_token"];
}
static string GetUserInfo(string token)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync(host + "api/Account/UserInfo").Result;
return response.Content.ReadAsStringAsync().Result;
}
}
static string GetValues(string token)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync(host + "api/Values").Result;
return response.Content.ReadAsStringAsync().Result;
}
}
}
In the above, we changed our code up a bit. We now have a GetToken()
method, which accepts a User Name/Password as arguments, and returns only the access_token string from the request to the Token endpoint of our Web Api.
Next, we added two different calls to our Web Api. One method calls the GetUserInfo()
method on the AccountController
, and the other calls the Get()
method on our ValuesController
.
If we spin up our Web Api, and, after it has spun up, run our client application, we should see the following output in our Console:
Console Output from Access Permissions Comparison:
Attempting to get User info as Admin User
Admin User Info Result: {"Email":"admin@example.com","HasRegistered":true,"Login
Provider":null}
Attempting to get Values info as Admin User
Admin Values Info Result: ["value1","value2"]
Attempting to get User info as Vanilla User
Vanilla User Info Result: {"Message":"Authorization has been denied for this req
uest."}
Attempting to get Values info as Vanilla User
Vanilla Values Info Result: ["value1","value2"]
Note the output from that third attempt. We are trying to call into GetUserInfo()
as a plain vanilla User, in the Users Role. Appropriately, our Web Api has returned an authorization error, since Users
are not allowed access to the method by virtue of the [Authorize(Roles="Admin")]
attribute on the class declaration for AccountController
.
In contrast, both users are able to access the Get()
method on ValuesController
, since this controller is decorated with a simple [Authorize]
attribute, which requires only an authenticated user for access.
Accessing Custom User Properties
We have also added some custom properties to our ApplicationUserModel
. Let’s take a look and see if we can work with those in the context of our examples here.
If we look more closely at the GetUserInfo()
method on AccountController
, we find that the actual return type for this method is UserInfoViewModel
, which is found in the Models => AccountViewModels.cs file. Now, in our crude, simple Console application we are not going to all the effort of de-serializing the JSON from our GET request into an object, but we COULD.
For our purposes, here, it will be sufficient to modify the UserInfoViewModel
to reflect the additional properties we want to return, and then update the GetUserInfo()
method to suit.
Add our custom User properties to the UserInfoViewModel class:
public class UserInfoViewModel
{
public string Email { get; set; }
public bool HasRegistered { get; set; }
public string LoginProvider { get; set; }
// Add our custom properties from ApplicationUser:
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
Next, update the GetUserInfo()
method to provide values for the additional properties we just added to UserInfoViewModel
:
Update GetUserInfo() Method on AccountController with Custom Properties:
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin
= ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
// We wouldn't normally be likely to do this:
var user = UserManager.FindByName(User.Identity.Name);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null,
// Pass the custom properties too:
Address = user.Address,
City = user.City,
State = user.State,
PostalCode = user.PostalCode
};
}
Above, we have called out to UserManager
to retreive an instance of our user, so we can get at the new properties we have added. We then set the corresponding values on the UserInfoViewModel
before returning.
This example is a little contrived, and we most likely would NOT do this in a production application this way. But for now, it will serve to demonstrate that our new and improved ApplicationUser
indeed has the custom properties we added to the model, and that we are retrieving them from the database as expected.
If we run our Console Client application one last time, we should see the following output:
Output from the Console Application with Custom User Properties:
Attempting to get User info as Admin User
Admin User Info Result: {"Email":"admin@example.com","HasRegistered":true,"Login
Provider":null,"Address":"1234 Sesame Street","City":"Portland","State":"OR",
"PostalCode":"97209"}
Attempting to get Values info as Admin User
Admin Values Info Result: ["value1","value2"]
Attempting to get User info as Vanilla User
Vanilla User Info Result: {"Message":"Authorization has been denied for this
request."}
Attempting to get Values info as Vanilla User
Vanilla Values Info Result: ["value1","value2"]
And we see, our custom user properties are returned with the JSON response.
Role-Based Authorization Versus Claims-Based Authorization
In this post, we have looked briefly at implementing Role-Based Authorization in the context of an ASP.NET Web Api project, and we had the briefest look at how Bearer Tokens work. It is important to note though, that for any but the simplest of authorization/access control schemes, Role-Based Authorization (“RBA”) is rapidly being overshadowed by Claims-Based Authorization.
Claims-Based Identity offers greater flexibility, and more effectively separates the Authentication and Authorization mechanism from your code. Note, in this example project, we need to specify, as part of the [Authorize]
attribute, precisely which Roles are allowed access to which controllers and/or methods.
Claims-Based Auth is more complex than RBA, but is generally going to be a more natural fit for a Web Api scenario, unless your needs are fairly simple.
Looking for .NET Core and .NET Framework hosting provider?
You should really consider finding an option that offers both features and transparent pricing. I personally recommend ASPHostPortal. ASPHostPortal has extremely affordable plans starting at $3.81 per month. The price covers everything from 5 GB of local storage for fast performance to a free SSL certification for customer trust to 24/7, 365 support to answer any questions you have about your website. Choosing the affordable web hosting by ASPHostPortal is a much better option for your business than risking everything for free hosting because you will always know what you are getting for the price.