Hi,
we are trying to understand what is the expected handling of a ChallengeResult when there are multiple authentication schemes registered.
We need to handle such a scenario because we have an ASP.NET core 2.2 app exposing some action methods (we use the MVC middleware) that must be used by an angularjs SPA which relies on cookies authentication and some third parties applications which use an authentication mechanism based on the Authorization HTTP request header. Please notice that the involved action methods are the same for both the users, this means that each one of them must allow authentication using both the cookie and the custom scheme based on Authorization HTTP request header. We know that probably this is not an optimal design but we cannot modify the overall architecture.
This documentation seems to confirm that what we would like to achieve is entirely possible using ASP.NET core 2.2. Unfortunately, the cookie authentication used by the UI app and the custom authentication used by the third parties must behave differently in case of an authentication challenge and their expected behaviors are not compatible with each other: the UI app should redirect the user to a login form, while a thir party application expects a raw 401 status code response. The documentation linked above does not offer a clear explanation of the ChallengeResult handling, so we decided to experiment with a test application.
We created two fake authentication handlers:
public class FooAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Foo failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
public class BarAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Bar failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
md5-244bcd611ae6af76f82837d925ddde7d
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "Bar";
options.AddScheme<FooAuthenticationHandler>("Foo", "Foo scheme");
options.AddScheme<BarAuthenticationHandler>("Bar", "Bar scheme");
});
}
md5-a9fc0e0209728aebac76ab93f8f9f935
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseAuthentication();
app.UseMvc();
}
md5-fb6a7c17af6d01be18eff9da84932cee
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values/5
[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Foo,Bar")]
public ActionResult<string> Get(int id)
{
return "value";
}
}
We noticed that:
FooAuthenticationHandler and BarAuthenticationHandler are called to handle the ChallengeResultFooAuthenticationHandler before BarAuthenticationHandler and depends on the Authorize attribute (if you swap the authentication schemes inside the Authorize attribute then BarAuthenticationHandler is called first)options.DefaultChallengeScheme = "Bar"; matters if and only if inside the [Authorize] attribute the property AuthenticationSchemes is not set. If you do so, only the BarAuthenticationHandler is called and FooAuthenticationHandler never gets a chance to authenticate the request or handle an authentication challenge. So, the question basically is: when you have such a scenario, how are you expected to handle the possible "incompatibility" of different authentication schemes regarding ChallengeResult handling since they get both called ?
In our opinion is fine that both have a chance to authenticate the request, but we would like to know if it is possible to decide which one should handle the authentication challenge.
Thanks for helping !
cc @blowdart \ @HaoK
I don't know. I'm going to wait till Hao is back.
I don't know. I'm going to wait till Hao is back.
Thanks for helping !
Apart from our specific use case, we would like to better understand how to handle the scenario with multiple authentication schemes. We are migrating a big code base from ASP.NET MVC 5 to ASP.NET core 2.2 so a comprehensive understanding of the framework is really important for us.
@Tratcher - can you provide a summary of what happens?
Ultimately it can't issue two challenges and you need a way to discriminate which challenge should be issued. This doc shows how to do dynamic selection once you have a discriminator.
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-2.2
Ultimately it can't issue two challenges and you need a way to discriminate which challenge should be issued. This doc shows how to do dynamic selection once you have a discriminator.
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-2.2
Thanks for the linked documentation, now it makes sense to me.
@EnricoMassoneDeltatre do you have any remaining question or should we close this issue?
@Eilon thanks for the explanation, we can close the issue.
Hi,
we tried to implement your suggestions in our code and we verified that the authentication challenge forward mechanism works.
So, using the authentication schemes of my example, we can decide that the authentication scheme called "Foo" forwards its authentication challenge to the authentication scheme called "Bar".
This means that, given an action method decorated with the attribute [Authorize(AuthenticationSchemes = "Foo,Bar")], when an anonymous user makes a call to the action method itself the method ChallengeAsync of the BarAuthenticationHandler is called two times (one for the forward done by the scheme "Foo" and one for the scheme "Bar" itself).
Right now, we have a new requirement of writing a JSON payload to the response each time the response status code is 401. Our first attempt was modify the BarAuthenticationHandler in the following manner:
public class BarAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Bar failed"));
}
public async Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = 401;
_context.Response.ContentType = "application/json";
var error = new { Message = "You must be authenticated in order to proceed !" };
var json = JsonConvert.SerializeObject(error);
await _context.Response.WriteAsync(json, Encoding.UTF8);
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
Unfortunately, doing this we get an InvalidOperationException because we write to the response stream two times (the method ChallengeAsync is called two times as explained above) . The second time we try to modify the response the exception is raised, because modifying the response is not allowed anymore at that time.
What do you suggest in order to handle such a scenario ? Is there any best practice ?
We basically have two ideas to handle this scenario:
if(!_context.Response.HasStarted)
{
_context.Response.StatusCode = 401;
_context.Response.ContentType = "application/json";
var error = new { Message = "You must be authenticated in order to proceed !" };
var json = JsonConvert.SerializeObject(error);
await _context.Response.WriteAsync(json, Encoding.UTF8);
}
Do you have any other suggestion ?
Thanks for helping
@Tratcher in case I guide you astray.
Basically don't have both schemes in the authorize attribute. Your virtual scheme is what's supposed to be doing the picking of the real scheme, that should be the automatic scheme for everything, and your code inside that should be doing the right thing.
@Tratcher in case I guide you astray.
Basically don't have both schemes in the authorize attribute. Your virtual scheme is what's supposed to be doing the picking of the real scheme, that should be the automatic scheme for everything, and your code inside that should be doing the right thing.
Hi and thanks for replying.
So, you are basically suggesting to specify only one scheme inside the Authorize attribute and avoid the ambiguity of having two different authorization schemes from the beginning ?
Referring to my example, I should use [Authorize(AuthenticationSchemes = "Bar")] and then decide how to authenticate the incoming request inside the code of BarAuthenticationHandler.AuthenticateAsync method. Did you mean this ?
Set Bar as your default eveywhere. Then on Bar set AuthenticationSchemeOptions.ForwardDefaultSelector and have it exhamine the request to determine if Foo or Bar should be used.
@Tratcher thanks for helping.
So, the general pattern is choose a default scheme and then set up forwarding logic for specific cases.
Indeed, the example that you provide here where the cookie authentication is the default and all requests starting by "/api" are forwarded to another scheme clearly complies with this pattern.
In our case a lot of the confusion highlighted in this discussion originates from the possibility of specifying multiple authentication schemes in the Authorize attribute via the property AuthenticationSchemes.
I don't know what the original envisioned use case is, but I guess that removing the possibility of specifying more than one scheme in the Authorize attribute could lead to a clearer api (using the singular in the property name, something like Scheme or the longer AuthenticationScheme). Is there any use case where specifying more than one scheme makes sense ?
There is a set of auth schemes that are not mutually exclusive: Basic, Bearer, Windows (Negotiate, NTLM), Digest, etc.. All of their challenges can be issued on the same response without conflicting.
There is a set of auth schemes that are not mutually exclusive: Basic, Bearer, Windows (Negotiate, NTLM), Digest, etc.. All of their challenges can be issued on the same response without conflicting.
@Tratcher thanks for the explanation.