Authentication is the process of ascertaining that somebody really is who they claim to be.
Authorization refers to rules that determine who is allowed to do what. E.g. Adam may be authorized to create and delete databases,
while Usama is only authorised to read.
The two concepts are completely orthogonal and independent, but both are central to security design, and the failure to get either one correct opens up the avenue to compromise.
In terms of web apps, very crudely speaking, authentication is when you check login credentials to see if you recognize a user as logged in, and authorization is when you look up in your access control whether you allow the user to view, edit, delete or create content.
It seems that System.Web.Security.Roles.GetRolesForUser(Username) does not get automatically hooked up when you have a custom AuthorizeAttribute and a custom RoleProvider.
So, in your custom AuthorizeAttribute you need to retrieve the list of roles from your data source and then compare them against the roles passed in as parameters to the AuthorizeAttribute.
I have seen in a couple blog posts comments that imply manually comparing roles is not necessary but when we override AuthorizeAttribute it seems that we are suppressing this behavior and need to provide it ourselves.
Anyway, I'll walk through what worked for me. Hopefully it will be of some assistance.
I welcome comments on whether there is a better way to accomplish this.
Note that in my case the AuthorizeAttribute is being applied to an ApiController although I'm not sure that is a relevant piece of information.
public class RequestHashAuthorizeAttribute : AuthorizeAttribute
{
bool requireSsl = true;
public bool RequireSsl
{
get { return requireSsl; }
set { requireSsl = value; }
}
bool requireAuthentication = true;
public bool RequireAuthentication
{
get { return requireAuthentication; }
set { requireAuthentication = value; }
}
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (Authenticate(ActionContext) || !RequireAuthentication)
{
return;
}
else
{
HandleUnauthorizedRequest(ActionContext);
}
}
protected override void HandleUnauthorizedRequest(HttpActionContext ActionContext)
{
var challengeMessage = new System.Net.Http.HttpResponseMessage(HttpStatusCode.Unauthorized);
challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
throw new HttpResponseException(challengeMessage);
}
private bool Authenticate(System.Web.Http.Controllers.HttpActionContext ActionContext)
{
if (RequireSsl && !HttpContext.Current.Request.IsSecureConnection && !HttpContext.Current.Request.IsLocal)
{
//TODO: Return false to require SSL in production - disabled for testing before cert is purchased
//return false;
}
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
IPrincipal principal;
if (TryGetPrincipal(authHeader, out principal))
{
HttpContext.Current.User = principal;
return true;
}
return false;
}
private bool TryGetPrincipal(string AuthHeader, out IPrincipal Principal)
{
var creds = ParseAuthHeader(AuthHeader);
if (creds != null)
{
if (TryGetPrincipal(creds[0], creds[1], creds[2], out Principal)) return true;
}
Principal = null;
return false;
}
private string[] ParseAuthHeader(string authHeader)
{
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 3 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1]) || string.IsNullOrEmpty(credentials[2])) return null;
return credentials;
}
private bool TryGetPrincipal(string Username, string ApiKey, string RequestHash, out IPrincipal Principal)
{
Username = Username.Trim();
ApiKey = ApiKey.Trim();
RequestHash = RequestHash.Trim();
//is valid username?
IUserRepository userRepository = new UserRepository();
UserModel user = null;
try
{
user = userRepository.GetUserByUsername(Username);
}
catch (UserNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
//is valid apikey?
IApiRepository apiRepository = new ApiRepository();
ApiModel api = null;
try
{
api = apiRepository.GetApi(new Guid(ApiKey));
}
catch (ApiNotFoundException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
if (user != null)
{
//check if in allowed role
bool isAllowedRole = false;
string[] userRoles = System.Web.Security.Roles.GetRolesForUser(user.Username);
string[] allowedRoles = Roles.Split(','); //Roles is the inherited AuthorizeAttribute.Roles member
foreach(string userRole in userRoles)
{
foreach (string allowedRole in allowedRoles)
{
if (userRole == allowedRole)
{
isAllowedRole = true;
}
}
}
if (!isAllowedRole)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
Principal = new GenericPrincipal(new GenericIdentity(user.Username), userRoles);
Thread.CurrentPrincipal = Principal;
return true;
}
else
{
Principal = null;
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
}
The custom authorize attribute is governing the following controller:
public class RequestKeyAuthorizeTestController : ApiController
{
[RequestKeyAuthorizeAttribute(Roles="Admin,Bob,Administrator,Clue")]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, "RequestKeyAuthorizeTestController");
}
}
In the custom RoleProvider, I have this method:
public override string[] GetRolesForUser(string Username)
{
IRoleRepository roleRepository = new RoleRepository();
RoleModel[] roleModels = roleRepository.GetRolesForUser(Username);
List<string> roles = new List<string>();
foreach (RoleModel roleModel in roleModels)
{
roles.Add(roleModel.Name);
}
return roles.ToArray<string>();
}
Best Answer
Claims-based security helps decouple your security model from your application domain. A claim can be anything you want to attach to the identity of the user, such as an email, phone number, or flag indicating whether the user is a super user. This gives you the ultimate flexibility on how you want to setup your authorization process. Historically in an ASP.NET application you have to determine what roles you want to allow and apply them when programming your application. Then you check if the user is in the role to authorize them. This mingles your security model with your application. In claims-based you have much more flexibility and it is more typical to setup an authorization scheme that takes a resource (ex: Orders in an order management system) and an operation (ex: read, write, execute) as input parameters to your authorization process, effectively decoupling security from your application. See ClaimsPrincipalPermissionAttribute for an example of this technique.
Claims-based security is required with OAuth but it works well with other authorization schemes as well. The custom claims you use in your application are accessible from ClaimsPrincipal.Current. There are techniques to store this information in cookies as well, although the ASP.NET security pipeline does not do this by default.
The discussion you reference is for Windows Identity Foundation (WIF) which is now part of .NET in 4.5 and is why claims-based identity is a first class citizen. All of the Principal types inherit from ClaimsPrincipal. For a good overview of claims-based security look at this free ebook "A Guide to Claims-Based Identity and Access Control (2nd Edition)". A real expert in this area is Dominick Baier and his blog is chocked full of useful information on this topic. He also has a great online training course on Pluralsight called "Identity & Access Control in ASP.NET 4.5".