Custom Authorize attribute HttpContext.Request.RawUrl unexpected results - asp.net-mvc

We're building an application that is a Silverlight client application, but we've created an MVC controller and a few simple views to handle authentication and hosting of the Silverlight control for this application. As part of the security implementation, I've created a custom Authorization filter attribute to handle this, but am getting some unexpected results trying to properly handle redirection after authentication.
For example, our Silverlight application's navigation framework allows users to deep-link to individual pages within the application itself, such as http://myapplicaton.com/#/Product/171. What I want, is to be able to force a user to login to view this page, but then successfully redirect them back to it after successful authentication. My problem is with getting the full, requested URL to redirect the user to from within my custom authorization filter attribute class.
This is what my attribute code looks like:
public class RequiresAuthenticationAttribute : FilterAttribute, IAuthorizationFilter
{
protected bool AuthorizeCore(HttpContextBase httpContext)
{
var cookie = Cookie.Get(SilverlightApplication.Name);
if (SilverlightApplication.RequiresLogin)
{
return
((cookie == null) ||
(cookie["Username"] != httpContext.User.Identity.Name) ||
(cookie["ApplicationName"] != SilverlightApplication.Name) ||
(Convert.ToDateTime(cookie["Timeout"]) >= DateTime.Now));
}
else
return false;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext != null && AuthorizeCore(filterContext.HttpContext))
{
var redirectPath = "~/login{0}";
var returnUrl = filterContext.HttpContext.Request.RawUrl;
if (string.IsNullOrEmpty(returnUrl) || returnUrl == "/")
redirectPath = string.Format(redirectPath, string.Empty);
else
redirectPath = string.Format(redirectPath, string.Format("?returnUrl={0}", returnUrl));
filterContext.Result = new RedirectResult(redirectPath);
}
}
}
So in this case, if I browse directly to http://myapplicaton.com/#/Product/171, in the OnAuthorize method, where I'm grabbing the filterContext.HttpContext.Request.RawUrl property, I would expect it's value to be "/#/Product/171", but it's not. It's always just "/". Does that property not include page level links? Am I missing something?

The # sign in URLs (also called the fragment part of an URL) is only used by browsers to navigate between history and links. Everything following this sign is never sent to the server and there's no way to get it in a server side script.

Related

ASP NET MVC5 randomly redirects to my login page

I have an ASP.NET MVC application with ActionFilters for Authentication and no Forms Authentication. "SegurancaAction" is the attribute responsible for validating authentication and exists in every controller endpoint except in the login ones (as expected).
I'm facing a problem in which sometimes I try to access one of my controllers and the GET request goes to my login endpoint. In the method Application_BeginRequest at Global.asax, I can see the very first attempt is at 'security/login' (the route to my login endpoint) instead of the one I want. I can also see this endpoint being called in debugging apps such as Fiddler, or ASP.NET Trace or Glimpse MVC5.
Besides calling the wrong action, once I login again this issue keeps happening for the same endpoint I was trying to access, redirecting my site to the login page over and over.
SegurancaAction:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Autenticacoes autenticacao = _authApp.IsAutenticado(filterContext.HttpContext.Session.SessionID);
if (autenticacao == null)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Unauthorized);
else
{
filterContext.HttpContext.Response.RedirectPermanent("/security/login");
return;
}
}
else
{
// other stuff
}
}
SecurityController:
[HttpPost]
[ConfigAction]
public ActionResult Login(vm_Login login)
{
if (ModelState.IsValid)
{
if (!String.IsNullOrEmpty(login.Login) && !String.IsNullOrEmpty(login.Senha))
{
Entidades entidade = _entidadeApp.GetByUsuarioSenha(login.Login, login.Senha);
if (entidade == null)
{
ViewBag.FalhaAutenticacao = "As credenciais informadas não conferem!";
return View("Login");
}
else
{
string encryptionKey = System.Configuration.ConfigurationManager.AppSettings["EncryptionKey"];
var a = _autenticacaoApp.Autenticar(entidade.Id, encryptionKey, login.Senha, HttpContext.Session.SessionID);
}
Response.RedirectPermanent("~/principal/index");
}
}
else
{
ViewBag.FalhaAutenticacao = "É necessário informar o usuario e a senha!";
}
return View();
}
All _autenticacaoApp.Autenticar(...) method does is to create an authentication entry on the database, it's a completely custom code.
Does anyone know why this issue happens? Sometimes I can reproduce it by deleting the cookies that contain ASP.NET_Session ID and RequestVerificationToken. So far I know those cookies are automatically generated and I notice that sometimes when I login again they are not re-generated.
I figured out the issue. It was this "RedirectPermanent" method being used here:
filterContext.HttpContext.Response.RedirectPermanent("/security/login");
It tells the browser that the resource I'm trying to access is no longer available and is now located at this new Url. The browser records this information and always redirects to the new resource.
I just changed it to use "Redirect" instead.

Extend AuthorizeAttribute to detect logged in non-user (How to handle user authorization)

Environment: ASP.NET MVC 4, Visual Studio 2012
The [Authorize] attribute verifies that the user has a valid login cookie, but it does NOT verify that the user actually exists. This would happen if a user is deleted while that user's computer still holds the persisted credentials cookie. In this scenario, a logged-in non-user is allowed to run a controller action marked with the [Authorize] attribute.
The solution would seem to be pretty simple: Extend AuthorizeAttribute and, in the AuthorizeCore routine, verify that the user exists.
Before I write this code for my own use, I'd like to know if someone knows of a ready-to-go solution to this gaping hole in the [Authorize] attribute.
You need a special authentication global action filter.
Solution to your problem is the following. You have to introduce the global action filter that will be executed before controller action is invoked. This event is named OnActionExecuting. And within this global action filter you can also handle the scenario that user have a valid auth cookie, but does not exists in persistence (DB) anymore (and you have to remove its cookie).
Here is the code example to get an idea:
public class LoadCustomPrincipalAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
CustomIdentity customIdentity;
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
UserData userData = UserRepository.GetUserByName(HttpContext.Current.User.Identity.Name);
if (userData == null)
{
//TODO: Add here user missing logic,
//throw an exception, override with the custom identity with "false" -
//this boolean means that it have IsAuthenticated on false, but you
//have to override this in CustomIdentity!
//Of course - at this point you also remove the user cookie from response!
}
customIdentity = new CustomIdentity(userData, true);
}
else
{
customIdentity = new CustomIdentity(new UserData {Username = "Anonymous"}, false);
}
HttpContext.Current.User = new CustomPrincipal(customIdentity);
base.OnActionExecuting(filterContext);
}
}
Hope it helps to you!
Do not forget to register this action filter as a global one. You can do this like:
private static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LoadCustomPrincipalAttribute());
}
Just to add this. Leave alone AuthorizeAttribute. It should work as it was meant. It simply check the HttpContext.Current.User.Identity.IsAuthenticated == true condition. There are situations that you would need to overide it, but this is not the one. You really need a proper user/auth handling before even AuthorizeAttribute kicks in.
Agreed with Peter. Here is what I did for an AngularJs app. Create an attribute that checks the lockout date. Change YourAppUserManager out with the correct one.
public class LockoutPolicyAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
var now = DateTime.UtcNow;
var currentUserId = Convert.ToInt32(HttpContext.Current.User?.Identity?.GetUserId());
var user = await HttpContext.Current.GetOwinContext().GetUserManager<YourAppUserManager>().FindByIdAsync(currentUserId);
if (user?.LockedOutUntil >= now)
{
actionContext.Response = actionContext.Request.CreateErrorResponse((HttpStatusCode)423, "Account Lockout");
return;
}
}
base.OnActionExecuting(actionContext);
}
}
Then have an AngularJs intercept service for status code 423 to redirect to login page.
switch (response.status) {
case 423: //Account lockout sent by the server.
AuthService.logOut();
window.location.href = '/login';

Serving an iCalendar file in ASPNET MVC with authentication

I'm trying to serve an iCalendar file (.ics) in my MVC application.
So far it's working fine. I have an iPhone subscribing to the URL for the calendar but now I need to serve a personalised calendar to each user.
When subscribing to the calendar on the iPhone I can enter a username and password, but I don't know how to access these in my MVC app.
Where can I find details of how the authentication works, and how to implement it?
It turns out that Basic Authentication is what is required. I half had it working but my IIS configuration got in the way. So, simply returning a 401 response when there is no Authorization header causes the client (e.g. iPhone) to require a username/password to subscribe to the calendar.
On the authorization of the request where there is an Authorization request header, the basic authentication can be processed, retrieving the username and password from the base 64 encoded string.
Here's some useful code for MVC:
public class BasicAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
var auth = filterContext.HttpContext.Request.Headers["Authorization"];
if (!String.IsNullOrEmpty(auth))
{
var encodedDataAsBytes = Convert.FromBase64String(auth.Replace("Basic ", ""));
var value = Encoding.ASCII.GetString(encodedDataAsBytes);
var username = value.Substring(0, value.IndexOf(':'));
var password = value.Substring(value.IndexOf(':') + 1);
if (MembershipService.ValidateUser(username, password))
{
filterContext.HttpContext.User = new GenericPrincipal(new GenericIdentity(username), null);
}
else
{
filterContext.Result = new HttpStatusCodeResult(401);
}
}
else
{
if (AuthorizeCore(filterContext.HttpContext))
{
var cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null);
}
else
{
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusDescription = "Unauthorized";
filterContext.HttpContext.Response.AddHeader("WWW-Authenticate", "Basic realm=\"Secure Calendar\"");
filterContext.HttpContext.Response.Write("401, please authenticate");
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.Result = new EmptyResult();
filterContext.HttpContext.Response.End();
}
}
}
private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
}
Then, my controller action looks like this:
[BasicAuthorize]
public ActionResult Calendar()
{
var userName = HttpContext.User.Identity.Name;
var appointments = GetAppointments(userName);
return new CalendarResult(appointments, "Appointments.ics");
}
I found this really helpful, but i hit a few problems during the development and i thought i would share some of them to help save other people some time.
I was looking to get data from my web application into the calendar for an android device and i was using discountasp as a hosting service.
The first problem i hit was that the validation did not work when uploaded to the server, stangely enough it was accepting my control panel login for discountasp but not my forms login.
The answer to this was to turn off Basic Authentication in IIS manager. This resolved the issue.
Secondly, the app i used to sync the calendar to the android device was called iCalSync2 - its a nice app and works well. But i found that it only worked properly when the file was delivered as a .ics (duh for some reason i put it as a .ical.. it must have been late) and i also had to choose the webcal option
Lastly i found i had to add webcal:// to the start of my url instead of http://
Also be careful as the code posted above ignores the roles input variable and always passes nothing so you might need to do some role based checks inside your calendar routine or modify the code above to process the roles variable.

ASP.NET MVC Custom Authorization

I have a question about custom authorization in MVC.
I have a site that I want to limit access to certain pages, depending on their group membership. Now I have seen tons of examples on how to do this if there is a single admin group and a single user group, for example, but not any examples for a third level.
For example, only users of a company can view orders for their own company (and each company has its own admins, etc). These companies are stored in a DB. So I have seen ways to do custom authorization, overriding the AuthorizeCore method on the AuthorizeAttribute, but I don't know how to access to the parameters passed into the controller to see if the user has access to the order (order id, for example).
Is this even the best place to do the check, or should this just be handled directly from the method of the controller?
The AuthorizationContext (parameter to OnAuthorize) provides access to the Controller, RouteData, HttpContext, etc. You should be able to use these in a custom authorization filter to do what you want. Below is a sample of code from a RoleOrOwnerAttribute derived from AuthorizeAttribute.
public override void OnAuthorization( AuthorizationContext filterContext )
{
if (filterContext == null)
{
throw new ArgumentNullException( "filterContext" );
}
if (AuthorizeCore( filterContext.HttpContext )) // checks roles/users
{
SetCachePolicy( filterContext );
}
else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// auth failed, redirect to login page
filterContext.Result = new HttpUnauthorizedResult();
}
// custom check for global role or ownership
else if (filterContext.HttpContext.User.IsInRole( "SuperUser" ) || IsOwner( filterContext ))
{
SetCachePolicy( filterContext );
}
else
{
ViewDataDictionary viewData = new ViewDataDictionary();
viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
}
}
// helper method to determine ownership, uses factory to get data context,
// then check the specified route parameter (property on the attribute)
// corresponds to the id of the current user in the database.
private bool IsOwner( AuthorizationContext filterContext )
{
using (IAuditableDataContextWrapper dc = this.ContextFactory.GetDataContextWrapper())
{
int id = -1;
if (filterContext.RouteData.Values.ContainsKey( this.RouteParameter ))
{
id = Convert.ToInt32( filterContext.RouteData.Values[this.RouteParameter] );
}
string userName = filterContext.HttpContext.User.Identity.Name;
return dc.Table<Participant>().Where( p => p.UserName == userName && p.ParticipantID == id ).Any();
}
}
protected void SetCachePolicy( AuthorizationContext filterContext )
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
}
If the authorization is really that dynamic, I would handle it in the controller. I have one action where I do this - you can return a HttpUnauthorizedResult to redirect to the login page or you can show a custom error in your view.
I don't the default redirect to the login page when somebody is already logged in, but not in the correct role. That's very confusing for the user.
My answer isn't great, because it kills unit testing, but I'm pulling values from System.Web.HttpContext.Current.Session. The singleton is available throughout the project. By saving the current user in session, you can get to it from anywhere, including utility classes like AuthorizeAttribute.
I'd love to see a unit-testable solution, though.

More control on ASP.Net MVC's Authorize; to keep AJAX requests AJAXy

I have some action methods behind an Authorize like:
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(int siteId, Comment comment) {
The problem I have is that I'm sending a request through AJAX to Comment/Create with
X-Requested-With=XMLHttpRequest
which helps identify the request as AJAX. When the user is not logged in and hits the Authorize wall it gets redirected to
/Account/LogOn?ReturnUrl=Comment%2fCreate
which breaks the AJAX workflow. I need to be redirected to
/Account/LogOn?X-Requested-With=XMLHttpRequest
Any ideas how that can be achieved? Any ways to gain more control over what happens when Authorization is requested?
Thanks to Lewis comments I was able to reach this solution (which is far from perfect, posted with my own comments, if you have the fixes feel free to edit and remove this phrase), but it works:
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
override public void OnAuthorization(AuthorizationContext filterContext) {
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest()) {
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
// 2- When leaving Result as a HttpUnauthorizedResult, ASP.Net actually does some nice automatic stuff, like adding a ReturnURL, when hardcodding the URL here, that is lost.
String url = System.Web.Security.FormsAuthentication.LoginUrl + "?X-Requested-With=XMLHttpRequest";
filterContext.Result = new RedirectResult(url);
}
}
}
Recently I ran into exactly the same problem and used the code posted by J. Pablo Fernández
with a modification to account for return URLs. Here it is:
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
override public void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
{
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
HttpRequestBase request = filterContext.HttpContext.Request;
string returnUrl = request.Path;
bool queryStringPresent = request.QueryString.Count > 0;
if (queryStringPresent || request.Form.Count > 0)
returnUrl += '?' + request.QueryString.ToString();
if (queryStringPresent)
returnUrl += '&';
returnUrl += request.Form;
String url = System.Web.Security.FormsAuthentication.LoginUrl +
"?X-Requested-With=XMLHttpRequest&ReturnUrl=" +
HttpUtility.UrlEncode(returnUrl);
filterContext.Result = new RedirectResult(url);
}
}
}
Instead of using the authorize attribute, I've been doing something like the following.
public ActionResult SomeCall(string someData)
{
if (Request.IsAjaxRequest() == false)
{
// TODO: do the intended thing.
}
else
{
// This should only work with AJAX requests, so redirect
// the user to an appropriate location.
return RedirectToAction("Action", "Controller", new { id = ?? });
}
}
I think the right way to handle this would be in your Javascript making the AJAX call.
If the user needs to be authorized (or authenticated as your code implies) and isn't, you should inform them and maybe not allow them to try and comment in the first place.
However, if that doesn't suit your needs.
You could try and write your own authorize action filter, maybe inheriting from the one that comes with the MVC framework but redirects how you want it to. It's fairly straightforward.

Resources