asp.net-mvc-5entity-framework-6asp.net-apicontrollerrole-base-authorization

Why ASP.NET MVC app doesn't recognize user role changes right after modification?


I have an ASP.NET MVC hybrid app which has an ApiController besides the MVC Controllers. I'm using role based authorization attributes within both the MVC Controllers and the ApiController both at the controller level and sometimes on method level. I'm using Entity Framework 6 with Model based design.

The Controller level authorizations:

[Authorize(Roles = "Administrator,RegularUser")]
public class EngineController : ApiController
{

or

[System.Web.Mvc.Authorize(Roles = "Administrator,RegularUser")]
public class ProjectsController : Controller
{

When I divert form the controller level authorization either because it's accessible for non logged in user:

    [AllowAnonymous]
    [HttpPost]
    public async Task<CheckCouponReturnValueModel> CheckCoupon([FromBody] CouponCodeRequestModel requestModel)

or because I soften the authorization ("User" is less privileged than "RegularUser"):

    [OverrideAuthorization()]
    [Authorize(Roles = "User")]
    [HttpPost]
    public TopicReturnValueModel GetTopic([FromBody]TopicReferenceModel requestModel)

Right after a user registers, usually it gets "User" and "RegularUser" roles. I can confirm that by querying the database's AspNetUserRoles table, or I even have a management view for administrators to control that and it shows the roles even through the same ASP.NET MVC app. However when the newly created user tries to access endpoints or views on the MVC Controllers, it gets dissed by the framework's authorization rules and gets an 401 Unauthorized. It's like the some internal parts (I don't know if it uses the RoleManager or what under the hood) "didn't get the message" that the user is already in the roles.

Strangely, the ApiController endpoints work though and recognize the user's roles. After the MVC controllers throws a 401, the user gets redirected to the Login page (with a redirection hint). At the same time the user is logged in, the menu bar reflects that (even when redirected to the login page - it's confusing). Once the user obeys and re logs in suddenly the schizophrenic behavior disappears and the MVC Controller endpoints start to recognize the user's roles as well. Needless to say, this is unacceptable this way.

My packages:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="animate.css" version="3.3.0.0" targetFramework="net461" />
  <package id="Antlr" version="3.5.0.2" targetFramework="net45" />
  <package id="bootstrap" version="3.3.7" targetFramework="net461" />
  <package id="Bootstrap.Datepicker" version="1.6.4" targetFramework="net461" />
  <package id="EntityFramework" version="6.1.3" targetFramework="net461" />
  <package id="FontAwesome" version="4.7.0" targetFramework="net461" />
  <package id="free-jqGrid" version="4.14.0" targetFramework="net461" />
  <package id="jQuery" version="2.2.4" allowedVersions="[2,3)" targetFramework="net461" />
  <package id="jquery.datatables" version="1.10.12" targetFramework="net461" />
  <package id="jQuery.InputMask" version="3.3.4" targetFramework="net461" />
  <package id="jquery.noty" version="2.3.5" targetFramework="net461" />
  <package id="jQuery.UI.Combined" version="1.12.1" targetFramework="net461" />
  <package id="jQuery.Validation" version="1.16.0" targetFramework="net461" />
  <package id="JSZip" version="3.1.3" targetFramework="net461" />
  <package id="knockoutjs" version="3.4.2" targetFramework="net461" />
  <package id="KnockoutJS.Validation" version="3.0.0" targetFramework="net45" />
  <package id="Microsoft.AspNet.Cors" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.EntityFramework" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Identity.Owin" version="2.2.1" targetFramework="net461" />
  <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.Web.Optimization" version="1.1.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebApi.Cors" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.jQuery.Unobtrusive.Validation" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Host.SystemWeb" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Cookies" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Facebook" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Google" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.MicrosoftAccount" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.OAuth" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Security.Twitter" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" />
  <package id="MimeTypeMap.List" version="1.1.0" targetFramework="net461" />
  <package id="Modernizr" version="2.8.3" targetFramework="net45" />
  <package id="Moment.js" version="2.18.1" targetFramework="net461" />
  <package id="morelinq" version="2.3.0" targetFramework="net461" />
  <package id="mousetrap" version="1.3" targetFramework="net461" />
  <package id="Mvc.JQuery.DataTables" version="1.5.31" targetFramework="net461" />
  <package id="Mvc.JQuery.DataTables.Common" version="1.5.31" targetFramework="net461" />
  <package id="Mvc.JQuery.Datatables.Templates" version="1.5.31" targetFramework="net461" />
  <package id="MvcSiteMapProvider.MVC5" version="4.6.22" targetFramework="net45" />
  <package id="MvcSiteMapProvider.MVC5.Core" version="4.6.22" targetFramework="net45" />
  <package id="MvcSiteMapProvider.Web" version="4.6.22" targetFramework="net45" />
  <package id="Nager.Date" version="1.6.0" targetFramework="net461" />
  <package id="Newtonsoft.Json" version="10.0.2" targetFramework="net461" />
  <package id="Owin" version="1.0" targetFramework="net45" />
  <package id="pdfmake" version="0.1.18" targetFramework="net461" />
  <package id="PDFsharp" version="1.32.3057.0" targetFramework="net461" />
  <package id="QueryInterceptor" version="0.2" targetFramework="net45" />
  <package id="ReCaptcha-AspNet" version="1.4.0" targetFramework="net461" />
  <package id="Respond" version="1.4.2" targetFramework="net461" />
  <package id="Sendgrid" version="9.1.1" targetFramework="net461" />
  <package id="SendGrid.CSharp.HTTP.Client" version="3.3.0" targetFramework="net461" />
  <package id="Spin.js" version="2.3.2.1" targetFramework="net461" />
  <package id="Stripe.net" version="8.2.0" targetFramework="net461" />
  <package id="System.Linq.Dynamic.Core" version="1.0.6.13" targetFramework="net461" />
  <package id="System.Net.Http" version="4.0.0" targetFramework="net461" allowedVersions="[4,4.0.0]" />
  <package id="WebActivatorEx" version="2.2.0" targetFramework="net461" />
  <package id="WebGrease" version="1.6.0" targetFramework="net45" />
</packages>

The ApiController's [Authorize(Roles="...")] attributes are using System.Web.Http.AuthorizeAttribute while my MVC Controllers were using System.Web.Mvc.AuthorizeAttribute. I thought that the ApiController were getting the roles right, but apparently I replaced all the authorization declarations in the MVC Controllers to System.Web.Http.AuthorizeAttribute and that didn't solve the problem either.


Startup.Auth asked by @solidau:

    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context, user manager and role manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            ExpireTimeSpan = new System.TimeSpan(8, 0, 0),    // Uncomment this to enable 8 hour inactivity/idle expiration
            SlidingExpiration = true,
            Provider = new CookieAuthenticationProvider
            {
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),

                // https://stackoverflow.com/questions/20149750/unauthorised-webapi-call-returning-login-page-rather-than-401
                // http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/
                // http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/
                OnApplyRedirect = ctx =>
                {
                    if (!IsAjaxRequest(ctx.Request))
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                    }
                }
            }
        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

        // Enables the application to remember the second login verification factor such as phone or email.
        // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
        // This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

    }

    private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;
    }

Yes, there's one trick there which makes the session available for the ApiController as well not just the MVC Controller, because I really need that. The auth subsystem I guess has a different DB context than the regular entities context used by the MVC controllers (instantiated in a base class).

public abstract class WorkflowControllersBase : Controller
{
    protected Entities _context = new Entities();

and every MVC Controller is a descent of that base class. Although I may have different contexts, I definitely confirm that I add the right roles in the DB, they are persisted. Can the auth subsystem context get out of sync with the DB's state? How to sync it?


@Ali, the current code:

                IdentityResult result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    await UserManager.AddToRoleAsync(user.Id, "User");
                    await UserManager.AddToRoleAsync(user.Id, model.AccountType);
                    await SignInAsync(user, isPersistent: true);
                    if (model.AccountType != "QuickDeal")
                    {
                        if (User.IsInRole("QuickDeal"))  // Remove from QuickDeal if the user upgraded
                            await UserManager.RemoveFromRoleAsync(user.Id, "QuickDeal");
                        await UserManager.AddToRoleAsync(user.Id, "RegularUser");
                    }

I tried to perform the role addition/removals after the SignInAsync, but didn't help so far. The actual SignInAsync is a method of AccountController, provided by the ASP.NET MVC template:

    private async Task SignInAsync(ApplicationUser user, bool isPersistent)
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
        AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, await user.GenerateUserIdentityAsync(UserManager));
    }

<configSections>
  <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
</configSections>
<connectionStrings>
  <add name="DefaultConnection" connectionString="Server=tcp:xyx.database.windows.net,1433;Initial Catalog=XYZ;Persist Security Info=False;User ID=csaba;Password=*************;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" providerName="System.Data.SqlClient" />
  <add name="Entities" connectionString="metadata=res://*/Models.EntityModel.csdl|res://*/Models.EntityModel.ssdl|res://*/Models.EntityModel.msl;provider=System.Data.SqlClient;provider connection string='Server=tcp:zyx.database.windows.net,1433;Initial Catalog=XYZ;Persist Security Info=False;User ID=csaba;Password=***************;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'" providerName="System.Data.EntityClient" />
</connectionStrings>

Note that the default string what Azure supplies doesn't have MARS turned on. But that way I got an error, so I set MultipleActiveResultSets=True. Maybe that's a lead towards the solution.


Solution

  • Since you are using cookies, you need to ensure the cookie is recreated with the new roles after the new role is assigned (otherwise, the stale cookie sticks around until it is expired). After you grant the new role, you can use the auth manager to sign the user out, then sign them in again, thus recreating their cookie, with the newly added roles. I have included a snippet, but you will have to customize for your code:

    IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
    authenticationManager.SignOut("ApplicationCookie");
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);