Kukkimonsuta/Odachi

Upgrade to ASP.NET Core 2.0 authentication not working across site

Closed this issue · 2 comments

Basic Authentication worked fine in core 1.1.

For core 2.0 I have experienced that the query path alone gets authenticated (and its sub routes). The rest of the site is not authenticated.

The rest of the site used to get authenticated automatically (in core 1.1) and now no longer does.

In slightly more detail:

  1. I have a single controller ValuesController
  2. Basic auth works fine for that mysite://api/values
  3. Basic auth works fine for any mysite://api/ call eg. mysite://api/nocontrollerhere (even if there is no controller)
  4. Basic auth fails for any other part of the site eg mysite://nocontrollerhere or mysite://myurl

Item 4 used to work in core 1.1. ie httpContext.User.Identity.IsAuthenticated used to be true, it is now false.

To clarify, this is not about a 404 not being authorized. Using middleware eg. Hangfire.io, relies on authentication. That is failing because of the above.

To reproduce:

Tech: Visual Studio 2017, C#, core 2.0, Odachi 2.0.0-preview-000151

Created a basic template web api project:
VS2017 -> File -> New -> Project... -> Visual C# -> .NET Core -> ASP.NET Core Web Application -> Web API -> No Authentication

The program is very basic:

appsettings.json

{
  "BasicAuthentication": {
    "Realm": "MyRealm",
    "Credentials": [
      {
        "Username": "u",
        "Password": "p"
      }
    ]
  }
}

ValuesController.cs

    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
            return Ok("success");
        }
    }

Snipped from Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<BasicOptions>(BasicDefaults.AuthenticationScheme, this.Configuration.GetSection("BasicAuthentication"));
            services.AddAuthentication(BasicDefaults.AuthenticationScheme)
                .AddBasic(BasicDefaults.AuthenticationScheme, _ => { });

            var mvcCore = services.AddMvcCore(options =>
            {
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();

                options.Filters.Add(new AuthorizeFilter(policy));
            });

            mvcCore.AddAuthorization();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
            app.UseMiddleware<AuthCheckMiddleware>();
            app.UseMvc();
        }

Snipped from AuthCheckMiddleware.cs

    public class AuthCheckMiddleware
    {
        private readonly RequestDelegate nextRequestDelegate;

        public AuthCheckMiddleware(RequestDelegate nextRequestDelegate)
        {
            this.nextRequestDelegate = nextRequestDelegate;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            await this.nextRequestDelegate(httpContext);

            bool isAuthenticated = httpContext.User.Identity.IsAuthenticated;
            string url = httpContext.Request.Path.Value;
            Debug.WriteLine($"{url} authentication is {isAuthenticated}");
        }
    }

Test:

  1. mysite://nocontrollerhere
  2. mysite://api/nocontrollerhere
  3. mysite://api/values <- challenged & successfully log in
  4. mysite://api/values
  5. mysite://api/nocontrollerhere
  6. mysite://nocontrollerhere
  7. mysite://any/other/url

The results are

  1. /nocontrollerhere authentication is False (not challenged) - Expected
  2. /api/nocontrollerhere authentication is False (not challenged) - Expected
  3. /api/values authentication is False -> then after logging in -> /api/values authentication is True - Expected
  4. /api/values authentication is True - Expected
  5. /api/nocontrollerhere authentication is True - Expected
  6. /nocontrollerhere authentication is False - Not expected
  7. /any/other/url authentication is False - Not expected

Attached is a zip of the code.
AuthTester.zip

Any advice would be most welcome. Any questions, please ask. Thank you.

I believe this is due to changes introduced with asp.net core 2.0 authentication and supporting multiple auth schemes. This is what I think is happening:

  1. /nocontrollerhere authentication is False (not challenged)
    • browser doesn't have stored credentials
    • controller doesn't exist, nobody is requesting auth
  2. /api/nocontrollerhere authentication is False (not challenged)
    • browser doesn't have stored credentials
    • controller doesn't exist, nobody is requesting auth
  3. /api/values authentication is False -> then after logging in -> /api/values authentication is True
    • controller exists and MVC in reaction to auth policy sends challenge
    • browser saves credentials for domain and saves that /api requires auth
  4. /api/values authentication is True
    • browser has stored credentials for domain and request matches /api path, so it sends auth header right away without challenge
  5. /api/nocontrollerhere authentication is True
    • browser has stored credentials for domain and request matches /api path, so it sends auth header right away without challenge
  6. /nocontrollerhere authentication is False
    • browser has saved credentials for domain, but request doesn't match /api path, so it doesn't send auth header and nobody is is requesting it
  7. /any/other/url authentication is False
    • browser has saved credentials for domain, but request doesn't match /api path, so it doesn't send auth header and nobody is is requesting it

If you want any url to send challenge, you must request it manually. Changing your AuthCheckMiddleware.Invoke like this should do the trick:

public async Task Invoke(HttpContext httpContext)
{
    // attempt to authenticate against default auth scheme (this will attempt to authenticate using data in request, but doesn't send challenge)
    var result = await httpContext.AuthenticateAsync();

    if (!httpContext.User.Identity.IsAuthenticated)
    {
        // request was not authenticated, send challenge and do not continue processing this request
        await httpContext.ChallengeAsync();
    }
    else
    {
        // request was authenticated, continue in pipeline
        await this.nextRequestDelegate(httpContext);

        bool isAuthenticated = httpContext.User.Identity.IsAuthenticated;
        string url = httpContext.Request.Path.Value;
        Debug.WriteLine($"url {url} authentication is {isAuthenticated}");
    }
}

Thanks for that.