title: Custom authorization filter not working in ASP.NET Core 3
body: <p>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 <code>IAuthorizationFilter</code>'s <code>OnAuthorization</code> method within which I redirect the user to the <code>SignIn</code> page when their authentication expires.</p>
  <p>When a controller action with <code>[CustomAuthorizationFilter]</code> is hit I expect the attribute's <code>OnAuthorization</code> method to be hit right away whether or not the authentication cookie has expired.</p>
  <p>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 <code>OnAuthorization</code> method is hit, defeating what I thought was the purpose of the <code>OnAuthorization</code> method.</p>
  <p>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 <a href="https://docs.microsoft.com/en-us/dotnet/core/compatibility/2.2-3.0#authorization-iallowanonymous-removed-from-authorizationfiltercontextfilters" rel="nofollow noreferrer">Microsoft docs</a>:</p>
  <blockquote>
  <p>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.</p>
  </blockquote>
  <p>So, it appears that implementations with <code>IAuthorizationFilter</code> may be broken in 3.0+ and I don't know how to fix it.</p>
  <p>Is this behavior normal or is my implementation incorrect?</p>
  <p>If normal, why am I reauthenticated before the <code>OnAuthorization</code> method runs?</p>
  <p>If incorrect, how can I implement it correctly?</p>
  <p>CustomAuthorizationFilter.cs</p>
  <pre><code>public class CustomAuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter
  {
      public void OnAuthorization(AuthorizationFilterContext context)
      {
          string signInPageUrl = &quot;/UserAccess/SignIn&quot;;
  
          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);
              }
          }
      }
  }
  </code></pre>
  <p>The IsAjaxRequest() extension used:</p>
  <pre><code>//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(&quot;request&quot;);
          }
  
          if (request.Headers != null)
          {
              return (request.Headers[&quot;X-Requested-With&quot;] == &quot;XMLHttpRequest&quot;);
          }
  
          return false;
      }
  }
  </code></pre>
  <p>AzureAD authentication implementation in Startup.cs</p>
  <pre><code>public void ConfigureServices(IServiceCollection services)
  {
      IAppSettings appSettings = new AppSettings();
      Configuration.Bind(&quot;AppSettings&quot;, appSettings);
  
      services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
          .AddAzureAD(options =&gt;
          {
              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&lt;OpenIdConnectOptions&gt;(AzureADDefaults.OpenIdScheme, options =&gt;
      {
          options.UseTokenLifetime = false;
          options.Authority = options.Authority + &quot;/v2.0/&quot;; //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 = &quot;name&quot;;
          //https://stackoverflow.com/a/53918948/12300287
          options.Events.OnSignedOutCallbackRedirect = context =&gt;
          {
              context.Response.Redirect(&quot;/UserAccess/LogoutSuccess&quot;);
              context.HandleResponse();
  
              return Task.CompletedTask;
          };
      });
  
      services.Configure&lt;CookieAuthenticationOptions&gt;(AzureADDefaults.CookieScheme, options =&gt;
      {
          options.AccessDeniedPath = &quot;/UserAccess/NotAuthorized&quot;;
          options.LogoutPath = &quot;/UserAccess/Logout&quot;;
          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(&quot;/Home/Error&quot;);
          // 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 =&gt;
      {
          endpoints.MapControllerRoute(
              name: &quot;default&quot;,
              pattern: &quot;{controller=UserAccess}/{action=Login}/{id?}&quot;);
      });
  }
  </code></pre>