This is my Global.asax.cs file:
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
...
}
protected void Application_Start()
{
this.PostAuthenticateRequest += new EventHandler(MvcApplication_PostAuthenticateRequest);
}
// This method never called by requests...
protected void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
var identity = new GenericIdentity(authTicket.Name, "Forms");
var principal = new GenericPrincipal(identity, new string[] { });
Context.User = principal;
}
}
}
When PostAuthenticateRequest gets execute?
According to the documentation:
Occurs when a security module has
established the identity of the user.
...
The PostAuthenticateRequest event is
raised after the AuthenticateRequest
event has occurred. Functionality that
subscribes to the
PostAuthenticateRequest event can
access any data that is processed by
the PostAuthenticateRequest.
And here's the ASP.NET Page Life Cycle.
But because your question is tagged with ASP.NET MVC I would strongly recommend you performing this into a custom [Authorize] attribute instead of using this event. Example:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = base.AuthorizeCore(httpContext);
if (isAuthorized)
{
var authCookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
var identity = new GenericIdentity(authTicket.Name, "Forms");
var principal = new GenericPrincipal(identity, new string[] { });
httpContext.User = principal;
}
}
return isAuthorized;
}
}
Now decorate your controllers/actions with the [MyAuthorize] attribute:
[MyAuthorize]
public ActionResult Foo()
{
// if you got here the User property will be the custom
// principal you injected in the authorize attribute
...
}
If you place your code on PostAuthenticateRequest you may get hit many times per request as every resource such as images and style sheets referenced on your page will trigger this event as they are treated as separate requests.
If you go with #Darin's answer, the AuthorizeAttribute won't render the action when isAuthorized returns false, but people may need it to be rendered anyway, even if its a public page (unrestricted access) you may want to show a "Display Name" saved on the userData part of the authTicket.
For that, I recommend loading the authCookie on an ActionFilterAttribute (AuthenticationFilter):
public class LoadCustomAuthTicket : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (!filterContext.Principal.Identity.IsAuthenticated)
return;
HttpCookie authCookie = filterContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null)
return;
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
var identity = new GenericIdentity(authTicket.Name, "Forms");
var principal = new GenericPrincipal(identity, new string[] { });
// Make sure the Principal's are in sync. see: https://www.hanselman.com/blog/SystemThreadingThreadCurrentPrincipalVsSystemWebHttpContextCurrentUserOrWhyFormsAuthenticationCanBeSubtle.aspx
filterContext.Principal = filterContext.HttpContext.User = System.Threading.Thread.CurrentPrincipal = principal;
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
//This method is responsible for validating the current principal and permitting the execution of the current action/request.
//Here you should validate if the current principle is valid / permitted to invoke the current action. (However I would place this logic to an authorization filter)
//filterContext.Result = new RedirectToRouteResult("CustomErrorPage",null);
}
}
And on global.asax.cs
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LoadCustomAuthTicket());
}
That way you also won't have to populate all your actions with the attribute.
Related
I'm facing an issue while working with Role-Based authentication for web APi.
I have a controller class where the controller has a custom authorize attribute called Myauthorize.
I have a method inside the controller which can be accessed only with Admin access.
But the same method has been calling with QA access as well.
Could anyone please help with the below?
Please find the code below.
Controller :
namespace Hosiptal.Controllers.office
{
[MyAuthorize(Constants.Roles.Admin)]
public class UserRolesController : ApiController
{
private readonly IRepository<EntityModels.Role> rolesRepository;
public UserRolesController(IRepository<EntityModels.Role> rolesRepository)
{
this.rolesRepository = rolesRepository;
}
// GET: Users
[HttpGet]
[Route("")]
public IEnumerable<Role> GetAll()
{
return this.rolesRepository.GetAll()
.ToArray()
.Select(r => Mapper.Current.Get<Role>(r));
}
}
}
MyAuthorize has followed.
namespace Hospital.Web.Filters.WebApi
{
public class MyAuthorize: AuthorizeAttribute
{
private readonly string[] allowedroles;
private static IUserProfileRepository UserProfileRepository
{
get { return IoC.Current.Resolve<IUserProfileRepository>(); }
}
public MyAuthorize(params string[] roles)
{
this.allowedroles = roles;
}
public override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken
cancellationToken)
{
var claimsIdentity = actionContext.RequestContext.Principal.Identity as ClaimsIdentity;
var alias = claimsIdentity.Name.Split('#')[0];
if (alias == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
user(alias);
return base.OnAuthorizationAsync(actionContext, cancellationToken);
}
public static GenericPrincipal user(string userName)
{
userName = userName.ToUpper();
var userProfile = UserProfileRepository.Get(userName) ?? new UserProfile()
{
UserName = userName,
Roles = new List<Role>(),
FirstLoginDateUtc = DateTime.UtcNow
};
return CreatePrincipal(userProfile);
}
public static GenericPrincipal CreatePrincipal(UserProfile user)
{
var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name,
user.UserName) }, "Custom");
return new GenericPrincipal(identity, user.Roles.Select(i =>
i.Name).ToArray());
}
}
}
How can restrict the user here based on access level?
If you review the source code for the AuthorizeAttribute class, you will see that it uses the controller context request's principal to perform authorization, so override the IsAuthorized method instead, move your code there and assign the principal you create to the context request's principal:
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var claimsIdentity = actionContext.RequestContext.Principal.Identity as ClaimsIdentity;
var alias = claimsIdentity.Name.Split('#')[0];
if (alias == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
//This sets the context's principal so the base class code can validate
actionContext.ControllerContext.RequestContext.Principal = user(alias);
//Call the base class and let it work its magic
return base.IsAuthorized(actionContext);
}
I will refrain from commenting on the design itself. This should fix your issue.
This is what's working for me
public class AdminAuthorizeAttributee : AuthorizeAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (AuthorizeRequest(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
base.HandleUnauthorizedRequest(actionContext);
}
private bool AuthorizeRequest(HttpActionContext actionContext)
{
try
{
var username = HttpContext.Current.User.Identity.Name;
var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
var user = userManager.Users.Where(a => a.UserName == username).FirstOrDefault();
var rolesForUser = userManager.GetRoles(user.Id);
var role = "Admin";
if (rolesForUser.Contains(role))
{
return true;
}
return false;
}
catch (Exception ex)
{
return false;
}
}
}
and in Controller
[AdminAuthorizeAttributee]
public class YOUR_Controller : ApiController
You don't need to create your own authorize filter for this.
Use the built-in [Authorize(Roles = "Admin")] - which will check if the user has a claim called "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" and if that value matches to the one you put in that authorize attribute, the authorization will succeed.
So in your case just make sure, when you log in the user to set his claim with the role like this:
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Role, "Admin"), //here set the users role
// ... other claims
};
(ClaimTypes class is from the namespace System.Security.Claims)
And then the [Authorize(Roles = "Admin")] should work just fine
I am validating a token stored on cookies, so I created a class
public class VIEWAuthorizeAttribute : AuthorizeAttribute
then I overrode the OnAuthorization class
public override void OnAuthorization(AuthorizationContext filterContext)
{
var cookie = filterContext.HttpContext.Request.Cookies.Get("Profile"); //This is working
if (cookie != null && IsValidToken(cookie["Token"]))
{
return;
}
HandleUnauthorizedRequest(filterContext);
}
That works fine for MVC Controllers, but when I try to do something similar for web api controller, I am not able to get the cookies from the request.
public override void OnAuthorization(HttpActionContext actionContext)
{
var foo = actionContext.Request.Headers.Cookies; //that is not working
if (Authorize(actionContext))
{
return;
}
HandleUnauthorizedRequest(actionContext);
}
actionContext.Request.Headers does not have a method Cookies, I also tried with actionContext.Request.Headers.GetCookies("Bar") like this answer, but Header does not have that GetCookies method.
Any idea?
string Authentication = string.Empty;
if (actionContext.Request.Headers.Contains("Cookie_Phone"))
{
Authentication = actionContext.Request.Headers.GetValues("Cookie_Phone")?.FirstOrDefault();
}
To use this extension method the namespace System.Net.Http is needed.
Also, pay attention on the GetCookies(string name) is an extension method that returns any cookie header present in the request that contain a cookie state whose name that matches the specified value. It returns a CookieHeaderValue that contains a collection of CookieValueCollection instances. So you need extra enumeration to get a CookieState by name that contains a value.
Here is the valid code:
public async Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken)
{
var request = context.Request;
var token = request.Headers
.GetCookies("token").FirstOrDefault()
?.Cookies.FirstOrDefault(x=>x.Name == "token")
?.Value;
if (string.IsNullOrEmpty(token))
{
context.ErrorResult = new AuthenticationFailureResult(
"The token is missing.",
request);
return;
}
var principal = await SomeAuthentication(token);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult(
"The principal has not been resolved.",
request);
return;
}
context.Principal = principal;
}
Here is my code. I have written login to validate the token, for valid token return user object. but unable to find way to make it available across controllers.
I don't want to use Identity.
public class CustomAuthorize : AuthorizeAttribute
{
private const string AUTH_TOKEN = "AuthToken";
public override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
AllowAnonymousAttribute allowAnonymousAttribute = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().FirstOrDefault();
if (allowAnonymousAttribute != null)
{
return Task.FromResult<object>(null);
}
if (actionContext.Request.Headers.Contains(AUTH_TOKEN))
{
var authToken = actionContext.Request.Headers.GetValues(AUTH_TOKEN).First();
var user = Utility.GetUserByToken(authToken);
if (user != null)
{
//
// how to make this `user` object available across the controllers
//
return Task.FromResult<object>(null);
}
else
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new CustomError() { Code = 100, Message = "Invalid Access Token" });
return Task.FromResult<object>(null);
}
}
else
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, new CustomError() { Code = 100, Message = "Invalid Access Token" });
return Task.FromResult<object>(null);
}
}
}
Please help...
Your Question is a bit unclear - I assume you are referring to this line:
var user = Utility.GetUserByToken(authToken);
If so, then I might have a solution. So the fundamental problem is that you cannot simply save this variable where you currently are in the current controller, you need to understand the context you are working in - Every time a different user makes a request a different user models get created in the current controller. To have access to the user model across my app whenever a user makes a request, I do the following:
First you need to hook into the request receiving process of ASP.NET. This can be done inside the Global.asax.cs file, but I prefer to keep it clean and create a PartialGlobal class and mark the Global.asax.cs as partial.
From
public class MvcApplication : System.Web.HttpApplication
To
public partial class MvcApplication : System.Web.HttpApplication
Then create the PartialGlobal Class:
public partial class MvcApplication
{
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
var request = HttpContext.Current.Request;
var authHeader = request.Headers["Authorization"];
//For API users
if (authHeader != null)
{
var authHeaderVal = AuthenticationHeaderValue.Parse(authHeader);
if (authHeaderVal.Scheme.Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(authHeaderVal.Parameter))
{
AuthenticateUser(authHeaderVal.Parameter);
}
}
}
//For Regular Website Users
else
{
HttpCookie authCookie = request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
//Extract the forms authentication cookie
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
// If caching userData field then extract
var userModel = UsersBLL.DeserializeObject(authTicket.UserData);
var principal = new UserPrincipal(userModel);
SetPrincipal(principal);
}
}
}
private static bool AuthenticateUser(string credentials)
{
var model = UsersBLL.DecryptToken(credentials);
if (!model.RefUser.HasValue)
{
return false;
}
SetPrincipal(new UserPrincipal(model));
return true;
}
private static void SetPrincipal(UserPrincipal principal)
{
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
}
}
The UserPrincipal Class:
public class UserPrincipal : IPrincipal
{
public IIdentity Identity { get; private set; }
//Just a class with details like name,age etc.
public UserModel Model { get; set; }
public UserPrincipal(UserModel model)
{
this.Model = model;
this.Identity = new GenericIdentity(model.Email);
}
}
Note the line in the PartialGLobal class: var model = UsersBLL.DecryptToken(credentials);. Here I simply use a method I created to de-crypt my token string so it can be deserialized, you probably won't have/need this.
The essential part is this last step of the PartialGlobal class:
private static void SetPrincipal(UserPrincipal principal)
{
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
}
If you have the context know about your user, you can access it anywhere by simply calling:
var principal = (UserPrincipal)HttpContext.Current.User;
One way is to Extend the ApiController which is what your controllers are using as the base class.
Define a CustomController
public class CustomController : ApiController
{
projected User _user;
}
For all the other controller use this as a base class and the _user object should be accessible from all the controllers.
I want to do custom authentication because we have many controllers and it makes sense to create global filter that applies for all controllers and their actions with exception of login page.
In Global.asax.cs I added next global filter:
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e) // Code that runs on application startup
{
... // only showing important part
GlobalFilters.Filters.Add(new Filters.AuthenticationUserActionFilter());
...
}
File AuthenticationUserActionFilter.cs:
public class AuthorizeUserActionFilter : System.Web.Mvc.Filters.IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true);
if (skipAuthorization) // anonymous filter attribute in front of controller or controller method
return;
// does this always read value from ASPXAUTH cookie ?
bool userAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
if (!userAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
return;
}
if( HttpContext.Current.User as Contracts.IUser == null )
{
// check if IUser is stored in session otherwise retrieve from db
// System.Web.HttpContext.Current.User is reseted on every request.
// Is it ok to set it from Session on every request? Is there any other better approach?
if (HttpContext.Current.Session["User"] != null && HttpContext.Current.Session["User"] as Contracts.IUser != null)
{
HttpContext.Current.User = HttpContext.Current.Session["User"] as Contracts.IUser;
}
else
{
var service = new LoginService();
Contracts.ISer user = service.GetUser(filterContext.HttpContext.User.Identity.Name);
HttpContext.Current.Session["User"] = user;
HttpContext.Current.User = user;
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) {}
}
My login code is like this (in AccountController.cs):
[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
LoginService service = new LoginService();
Contracts.IUser user = service .Login(username, password);
System.Web.HttpContext.Current.Session["User"] = value;
System.Web.HttpContext.Current.User = value;
// set cookie i.e. ASPX_AUTH, if remember me, make cookie persistent, even if user closed browser
if (System.Web.Security.FormsAuthentication.IsEnabled)
System.Web.Security.FormsAuthentication.SetAuthCookie(username, rememberMe);
return new SuccessResponseMessage().AsJsonNetResult();
}
Contracts.IUser interface:
public interface IUser : IPrincipal
{
Contracts.IUserInfo UserInfo { get; }
Contracts.ICultureInfo UserCulture { get; }
}
My question is this:
System.Web.HttpContext.Current.User is reseted on every request. Is it ok to set HttpContext.Current.User with Session value on every request? Is there any other better approach? What is best practise? Also Microsoft seems to have multiple ways of dealing with this problem (googled a lot of articles on this, also on stackoverflow Custom Authorization in Asp.net WebApi - what a mess?). There is a lot of confusion about this, although they developed a new Authorization in asp.net core.
One possible approach is to serialize the user as part of the UserData portion of the ASPXAUTH cookie. This way you don't need to fetch it from the database on each request and you don't need to use Sessions (because if you use sessions in a web-farm you will have to persist this session somewhere like in a database, so you will be round-tripping to the db anyway):
[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
LoginService service = new LoginService();
Contracts.IUser user = service.Login(username, password);
string userData = Serialize(user); // Up to you to write this Serialize method
var ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddHours(24), rememberMe, userData);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket));
return new SuccessResponseMessage().AsJsonNetResult();
}
And then in your custom authorization filter you could decrypt the ticket and authenticate the user:
public void OnAuthentication(AuthenticationContext filterContext)
{
... your stuff about the AllowAnonymousActionFilter comes here
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null)
{
// Unauthorized
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
return;
}
// Get the forms authentication ticket.
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
Contracts.ISer user = Deserialize(authTicket.UserData); // Up to you to write this Deserialize method -> it should be the reverse of what you did in your Login action
filterContext.HttpContext.User = user;
}
I have extended the Authorize attribute to include roles which comes from a cookie. Debugging gives good result, it returns true or false accordingly. However if I first log in with "Admin" Role and then try to go to a controller that requires a User role, the Authorize returns false but still the controller allows access.
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException("httpContext");
if (httpContext.User != null)
{
if (httpContext.User.Identity.IsAuthenticated)
{
if (httpContext.User.Identity is FormsIdentity)
{
FormsIdentity id = httpContext.User.Identity as FormsIdentity;
FormsAuthenticationTicket ticket = id.Ticket;
string role = ticket.UserData;
if (RequiredRole.Contains(role)) return true;
}
}
else
return false;
}
return false;
}
Requiredrole is a property of the class.
[CustomAuthorize(RequiredRole = "Admin", LoginPage = "Club")]
public class UsuarioAdminController : Controller
{
above code for a controller that requires admin role.
[CustomAuthorize(RequiredRole = "User", LoginPage = "Club")]
public class HotelController : Controller
{
above code for a controller with User role.
Can someone see why if Authorize returns false it allows access? Thanks
The AuthorizeCore Attribute behave as expected, it returns true or false; however the controller allows access when the AuthorizeCore method returns false.
Yes, there is more code, but I dont think it makes a differnce..here it is.
public class CustomAuthorizeAttribute: AuthorizeAttribute
{
public string RequiredRole;
public string LoginPage;
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null) throw new ArgumentNullException("httpContext");
if (httpContext.User != null)
{
if (httpContext.User.Identity.IsAuthenticated)
{
if (httpContext.User.Identity is FormsIdentity)
{
FormsIdentity id = httpContext.User.Identity as FormsIdentity;
FormsAuthenticationTicket ticket = id.Ticket;
string role = ticket.UserData;
if (RequiredRole.Contains(role)) return true;
}
}
else
return false;
}
return false;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var routeValues = new RouteValueDictionary();
if (LoginPage == "Club")
{
routeValues["action"] = "Index";
routeValues["controller"] = LoginPage;
routeValues["ReturnUrl"] = filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectToRouteResult(routeValues);
}
else {
routeValues["area"] = "mobile";
routeValues["action"] = "login";
routeValues["controller"] = LoginPage;
routeValues["ReturnUrl"] = filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectToRouteResult(routeValues);
}
}
}
}
I found the answer to this particular situation and I am sharing my findings. When a request was properly unauthorized it was handled by the
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
method. Inside that method I was checking first if the user is not authenticated. For the first request it works properly sending the user to the appropriate log in page, But now if you try to navigate to another page that requires a different role, because you are already authenticated it wont go inside the redirect and will allow access to the control. So by removing the !IsAuthenticated if at the beginning, now all unauthorized request is properly sent to the correct login page...