Handling session timeout with Ajax in .NET Core MVC - c#

I have a regular application using cookie based authentication. This is how it's configured:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.AccessDeniedPath = new PathString("/Home/Denied");
});
}
This works for my regular actions:
[Authorize]
public IActionResult Users()
{
return View();
}
But doesn't work well for my ajax requests:
[Authorize, HttpPost("Api/UpdateUserInfo"), ValidateAntiForgeryToken, Produces("application/json")]
public IActionResult UpdateUserInfo([FromBody] Request<User> request)
{
Response<User> response = request.DoWhatYouNeed();
return Json(response);
}
The problem is that when the session expires, the MVC engine will redirect the action to the login page, and my ajax call will receive that.
I'd like it to return the status code of 401 so I can redirect the user back to the login page when it's an ajax request.
I tried writing a policy, but I can't figure how to unset or make it ignore the default redirect to login page from the authentication service.
public class AuthorizeAjax : AuthorizationHandler<AuthorizeAjax>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeAjax requirement)
{
if (context.User.Identity.IsAuthenticated)
{
context.Succeed(requirement);
}
else
{
context.Fail();
if (context.Resource is AuthorizationFilterContext redirectContext)
{
// - This is null already, and the redirect to login will still happen after this.
redirectContext.Result = null;
}
}
return Task.CompletedTask;
}
}
How can I do this?
Edit: After a lot of googling, I found this new way of handling it in version 2.0:
services.AddAuthentication("Login")
.AddCookie("Login", c => {
c.ClaimsIssuer = "Myself";
c.LoginPath = new PathString("/Home/Login");
c.Events.OnRedirectToLogin = (context) =>
{
// - Or a better way to detect it's an ajax request
if (context.Request.Headers["Content-Type"] == "application/json")
{
context.HttpContext.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
});
And it works for now!

What you need can be achieved by extending AuthorizeAttribute class.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.Result = new JsonResult
{
Data = new { Success = false, Data = "Unauthorized" },
ContentEncoding = System.Text.Encoding.UTF8,
ContentType = "application/json",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
You can then specify this attribute on Ajax methods.
Hope this helps.
Reference: http://benedict-chan.github.io/blog/2014/02/11/asp-dot-net-mvc-how-to-handle-unauthorized-response-in-json-for-your-api/

Related

C# .NET 6.0 How to redirect an unauthorized user to an unauthorizedpage

I would like to redirect the unauthorized user to a unauthorizedpage. Authorization is Roles based. For example [Authorize(Roles = "Admin")] I'm looking for a general solution. I don't want to write a redirect in every controllor/endpoint. I'm using Windows Auth with the Negotiate protocol.
I'm using the AuthorizeAttribute. When the user is unauthorized for a view then the application shows a blank HTML page to the user.
I tried multiple CustomAttributes, Configure AccesDeniedPaths, HandleUnauthorizedRequest. But every way ended in a blank HTML page.
I hope someone has an solution.
CustomAuthorizeAttribute
public class CustomAuthorizeAttribute : ActionFilterAttribute
{
public string Roles { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//if user isn't logged in.
if (filterContext.HttpContext.User.Identity == null || !filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult("/Unauthorized/");
}
var user = filterContext.HttpContext.User;
//Check user rights here
if (!user.IsInRole(Roles))
{
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.Result = new RedirectResult("/Unauthorized/");
}
}
}
Controller
[CustomAuthorize(Roles = "Admin")]
[HttpGet]
public IActionResult Index()
{
return View();
}
ASP.NET has a defualt [Authorize] attribute which you can add to an arbitrary action.
Eg:
[HttpGet]
[Authorize]
public async Task<ActionResult<IEnumerable<Classes>>> GetClasses()
{
return await _context.Classes.ToListAsync();
}
This will by default redirect the user to the login page if an unauthorized user tries to access a forbidden page.
However, if you want to redirect to a custom page. You could try writing a custom filter attribute like this.
public class CustomAuthorizeAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.User.Identity == null || !filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult(System.Web.Security.FormsAuthentication.LoginUrl + "?returnUrl=" +
filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
}
//Check user rights here
if (userNotRight)
{
filterContext.HttpContext.Response.StatusCode = 302;
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
and use it in a controller like this,
[HttpGet]
[CustomAuthorize]
public async Task<ActionResult<IEnumerable<Classes>>> GetClasses()
{
return await _context.Classes.ToListAsync();
}
If you would like to authorize based on roles for the custom filter attribute. You could either
1. write a custom authorize attribute and add it along with the filter attribute.
public class AuthorizeAdmin : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
// The user is not authenticated
return false;
}
var user = httpContext.User;
if (user.IsInRole("Admin")) // Your desired role
{
return true;
}
}
}
controller:
[HttpGet]
[CustomAttribute]
[AuthorizeAdmin]
public async Task<ActionResult<IEnumerable<Classes>>> GetClasses()
{
return await _context.Classes.ToListAsync();
}
2. you could directly check it along with the custom filter attribute
public class CustomAuthorizeAttribute : ActionFilterAttribute
{
// Check if is in "Admin" role
var authorized = base.AuthorizeCore(httpContext);
if (!authorized)
{
// The user is not authenticated
return false;
}
var user = httpContext.User;
if (user.IsInRole("Admin")) // Your desired role
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.User.Identity == null || !filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new
RedirectResult(System.Web.Security.FormsAuthentication.LoginUrl
+ "?returnUrl=" +
filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
}
//Check user rights here
if (userNotRight)
{
filterContext.HttpContext.Response.StatusCode = 302;
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
}
controller:
[HttpGet]
[CustomAttribute]
public async Task<ActionResult<IEnumerable<Classes>>> GetClasses()
{
return await _context.Classes.ToListAsync();
}

Custom authorization filter not working in ASP.NET Core 3

I'm working on adding authentication (and eventually authorization) with AzureAD to an ASP.NET Core 3.1 app using a custom authorization attribute filter. The code below implements the IAuthorizationFilter's OnAuthorization method within which I redirect the user to the SignIn page when their authentication expires.
When a controller action with [CustomAuthorizationFilter] is hit I expect the attribute's OnAuthorization method to be hit right away whether or not the authentication cookie has expired.
That expectation doesn't happen and instead if a user is not authenticated and a controller action is hit, user is automatically reauthenticated with Microsoft and a valid cookie is created, and only then the OnAuthorization method is hit, defeating what I thought was the purpose of the OnAuthorization method.
I've been doing a lot of research to understand this behavior, but I'm clearly missing something. The most useful piece of information I found was in Microsoft docs:
As of ASP.NET Core 3.0, MVC doesn't add AllowAnonymousFilters for
[AllowAnonymous] attributes that were discovered on controllers and
action methods. This change is addressed locally for derivatives of
AuthorizeAttribute, but it's a breaking change for
IAsyncAuthorizationFilter and IAuthorizationFilter implementations.
So, it appears that implementations with IAuthorizationFilter may be broken in 3.0+ and I don't know how to fix it.
Is this behavior normal or is my implementation incorrect?
If normal, why am I reauthenticated before the OnAuthorization method runs?
If incorrect, how can I implement it correctly?
CustomAuthorizationFilter.cs
public class CustomAuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
string signInPageUrl = "/UserAccess/SignIn";
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
JsonResult jsonResult = new JsonResult(new { redirectUrl = signInPageUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(signInPageUrl);
}
}
}
}
The IsAjaxRequest() extension used:
//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions
{
public static bool IsAjaxRequest(this HttpRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.Headers != null)
{
return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
return false;
}
}
AzureAD authentication implementation in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
IAppSettings appSettings = new AppSettings();
Configuration.Bind("AppSettings", appSettings);
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options =>
{
options.Instance = appSettings.Authentication.Instance;
options.Domain = appSettings.Authentication.Domain;
options.TenantId = appSettings.Authentication.TenantId;
options.ClientId = appSettings.Authentication.ClientId;
options.CallbackPath = appSettings.Authentication.CallbackPath;
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.UseTokenLifetime = false;
options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = true;
// https://stackoverflow.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
// https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
options.TokenValidationParameters.NameClaimType = "name";
//https://stackoverflow.com/a/53918948/12300287
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.Response.Redirect("/UserAccess/LogoutSuccess");
context.HandleResponse();
return Task.CompletedTask;
};
});
services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
{
options.AccessDeniedPath = "/UserAccess/NotAuthorized";
options.LogoutPath = "/UserAccess/Logout";
options.ExpireTimeSpan = TimeSpan.FromMinutes(appSettings.Authentication.TimeoutInMinutes);
options.SlidingExpiration = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // who are you?
app.UseAuthorization(); // are you allowed?
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=UserAccess}/{action=Login}/{id?}");
});
}
I hoped to find a way to create an AuthorizeAttribute filter to solve this issue, but due to time constraints I settled on a regular action filter. It works with AJAX calls and it redirects the user to the appropriate pages if they are unauthorized or unauthenticated:
AjaxAuthorize action filter:
//custom AjaxAuthorize filter inherits from ActionFilterAttribute because there is an issue with
//a inheriting from AuthorizeAttribute.
//post about issue:
//https://stackoverflow.com/questions/64017688/custom-authorization-filter-not-working-in-asp-net-core-3
//The statuses for AJAX calls are handled in InitializeGlobalAjaxEventHandlers JS function.
//While this filter was made to be used on actions that are called by AJAX, it can also handle
//authorization not called through AJAX.
//When using this filter always place it above any others as it is not guaranteed to run first.
//usage: [AjaxAuthorize(new[] {"RoleName", "AnotherRoleName"})]
public class AjaxAuthorize : ActionFilterAttribute
{
public string[] Roles { get; set; }
public AjaxAuthorize(params string[] roles)
{
Roles = roles;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string signInPageUrl = "/UserAccess/SignIn";
string notAuthorizedUrl = "/UserAccess/NotAuthorized";
if (context.HttpContext.User.Identity.IsAuthenticated)
{
if (Roles.Length > 0)
{
bool userHasRole = false;
foreach (var item in Roles)
{
if (context.HttpContext.User.IsInRole(item))
{
userHasRole = true;
}
}
if (userHasRole == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
JsonResult jsonResult = new JsonResult(new { redirectUrl = notAuthorizedUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(notAuthorizedUrl);
}
}
}
}
else
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 403;
JsonResult jsonResult = new JsonResult(new { redirectUrl = signInPageUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(signInPageUrl);
}
}
}
}
The IsAjaxRequest() extension used (reposted for a complete answer):
//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions
{
public static bool IsAjaxRequest(this HttpRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.Headers != null)
{
return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}
return false;
}
}
JavaScript ajax global error handler:
//global settings for the AJAX error handler. All AJAX error events are routed to this function.
function InitializeGlobalAjaxEventHandlers() {
$(document).ajaxError(function (event, xhr, ajaxSettings, thrownError) {
//these statuses are set in the [AjaxAuthorize] action filter
if (xhr.status == 401 || xhr.status == 403) {
var response = $.parseJSON(xhr.responseText);
window.location.replace(response.redirectUrl);
} else {
RedirectUserToErrorPage();
}
});
}

Api with 401 status returning login page

I have created a POST API under UmbracoApiController.
[HttpPost]
[ActionName("SaveData")]
public HttpResponseMessage SaveData([FromBody]JObject data)
{
if (!authorized)
{
return Request.CreateResponse(HttpStatusCode.Unauthorized,
"Unauthorized access. Please check your credentials");
}
}
Instead of returning 401, it is going to the login page with 302 status.
I have created a custom attribute as well -
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class BasicAuthorization : AuthorizationFilterAttribute
{
private const string _authorizedToken = "Authorization";
public override void OnAuthorization(HttpActionContext filterContext)
{
var authorizedToken = string.Empty;
try
{
var headerToken = filterContext.Request.Headers.FirstOrDefault(x => x.Key == _authorizedToken);
if (headerToken.Key != null)
{
authorizedToken = Convert.ToString(headerToken.Value.SingleOrDefault());
if (!IsAuthorize(authorizedToken))
{
var httpContext = HttpContext.Current;
var httpResponse = httpContext.Response;
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent("Unauthorized access. Please check your credentials")
};
httpResponse.StatusCode = (int) HttpStatusCode.Unauthorized;
httpResponse.SuppressFormsAuthenticationRedirect = true;
return;
}
}
else
{
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
return;
}
}
catch (Exception)
{
filterContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
return;
}
base.OnAuthorization(filterContext);
}
private static bool IsAuthorize(string authorizedToken)
{
return authorizedToken == ConfigurationManager.AppSettings["VideoIngestionKey"];
}
}
But this also does not work. I am using Umbraco 7.6.13
Any help greatly appreciated.
Thanks
Have something similar but used with Surface Controller not Web API controller.
Override HandleUnauthorizedRequest to implement custom response / override Umbraco & .NET defaults.
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// example redirects to a 'Forbidden' doctype/view with Reponse.StatusCode set in view;
filterContext.Result =
new RedirectToUmbracoPageResult(
UmbracoContext.Current.ContentCache.GetSingleByXPath("//forbidden"));
}
It's odd that Forms authentication seems to be kicking in and redirecting you to login page for an API request. The AuthorizationFilterAttribute should return a Http 401 by default (so could deal with via web.config customErrors or httpErrors sections instead of code).
May want to review your web.config settings?

asp.net mvc validate [HttpPost] ActionResult()

I need to implement a ActionFilterAttribute [POST] ActionResult() in the controller. The problem is that I try to “redirect” to a page if validation failed... But it does not work. Validation runs, but then returns to the ActionResult() next line and finally when the view is returned, only then “redirected” to the page listed in the validation. Ultimately what I need is to stop the ActionResult() statements and “redirect” to the page listed in the validation. I tried OnActionExecuting() and OnActionExecuted() but does not work any
I need to...
filterContext.HttpContext.Response.Redirect (loginUrl, true);
Run away, “redirecting” the page indicated
My code:
[HelperSomeValidations("")]
[HttpPost]
public ActionResult Create(Pais pais)
{
try
{
PaisBLL.saveNew(pais);
}
catch (Exception ex)
{
ViewBag.error = ex;
return View(“Error”);
}
return RedirectToAction(“Index”);
}
public class HelperSomeValidations : ActionFilterAttribute
{
public HelperSomeValidations(String permiso)
{
this.permiso = permiso;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var user = filterContext.HttpContext.Session["coco"];
if (user == null) //validates if the user just login
{
//send them off to the login page
var url = new UrlHelper(filterContext.RequestContext);
var loginUrl = url.Content(“~/Usuario/Login”);
filterContext.HttpContext.Response.Redirect(loginUrl, true);
}
else
{
if (permission != “”)
{
//does some validations with “permission”
}
}
}
}
Thks!
I know this doesn't solve the problem you have posted but I feel it's a better solution. I would personally use an AuthoriseAttribute here instead as this is what it's designed todo.
public class Authorise : AuthorizeAttribute
{
private readonly string _permissionSystemName;
public Authorise()
{
}
public Authorise(string permissionSystemName)
{
_permissionSystemName = permissionSystemName;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//DO some logic and return True or False based on whether they are allowed or not.
return false;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
area = filterContext.HttpContext.Request.RequestContext.RouteData.Values["area"],
controller = "Generic",
action = "PermissionDenied"
})
);
}
}
Usage would be along the lines of:
[Authorise("SomePermissionName")]
public class MyController : Controller
{
}
Instead of calling filterContext.HttpContext.Response.Redirect(loginUrl, true), you need to set the filterContext.Result to a RedirectResult.

handling my unauthorized requests , with an http 200 response + custom message

I have the following custom authorization class inside my asp.net mvc web application, which i call before my action methods:-
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class CheckUserPermissionsAttribute : AuthorizeAttribute
{
public string Model { get; set; }
public string Action { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!httpContext.Request.IsAuthenticated)
return false;
//code goes here................
if (!repository.can(ADusername, Model, value)) // implement this method based on your tables and logic
{ return false; }
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
var viewResult = new JsonResult();
viewResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
viewResult.Data = (new { IsSuccess = "Unauthorized", description = "Sorry, you do not have the required permission to perform this action." });
filterContext.Result = viewResult;
}
else
{
var viewResult = new ViewResult();
viewResult.ViewName = "~/Views/Errors/_Unauthorized.cshtml";
filterContext.Result = viewResult;
}
// base.HandleUnauthorizedRequest(filterContext);
}
}
and i call this custom authorization before my action method as follow:-
[CheckUserPermissions(Action = "Read", Model = "Accounts")]
public ActionResult Index(){
Currently as seen in the above code when the request is not authorized , I will return a JSON or a partial view depending on request type (if it is Ajax request or not).
And inside my code I always take care of handling the json returned from the custom authorization class inside the onsuccess script as follow:-
function addrecords(data) {
if (data.IsSuccess == "Unauthorized") {
jAlert(data.description, 'Unauthorized Access');
}
else if (data.IsSuccess) {
jAlert(data.description, 'Creation Confirmation');
}
Currently my approach is working well, but I start thinking if I should continue with the fact that I am NOT retuning 401 http response for unauthorized requests ? and instead of that I am returning an http 200 , either as json object with status = “unauthrized” or redirect to a partial view ?
Can anyone advice ?
Thanks.
i used to do like this:
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.Result = new JsonResult { Data = "LogOut" };
}
else
{
filterContext.Result = new RedirectResult("~/Home/Index");
}
and in jquery i check in generic ajaxError:
$(document).ajaxError(function(xhr, statusText, err){
if(xhr.status == 403) {
alert("Unathorized Request");
}
});
or:
$.ajaxSetup({
error: function (x, e) {
if (x.status == 403) {
alert("Unauthorized Access");
}
});
});
In your approach you have to check in every Ajax call success the response what is coming, but in this approach in unauthorized case returning 403 code will make Ajax call fail and error callback executes and we i use to write a generic error handler for Ajax and check if status code is that what u i return then show message that it is unauthorized request.
you can see details : Asp.net mvc Check User is Logged In and authorized Before Access to Page

Categories