Reputation: 728
I am not using Identity.
I have this ASP.NET Core configuration enabling two authentication schemes, cookies and basic auth:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "_auth";
options.Cookie.HttpOnly = true;
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/LogOff");
options.AccessDeniedPath = new PathString("/Account/Login");
options.ExpireTimeSpan = TimeSpan.FromHours(4);
options.SlidingExpiration = true;
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
BasicAuthenticationHandler
is a custom class inheriting from AuthenticationHandler
and overriding HandleAuthenticateAsync
to check the request headers for basic authentication challenge, and returns either AuthenticateResult.Fail()
or AuthenticateResult.Success()
with a ticket and the user claims.
It works fine as is:
[Authorize]
tag will check the cookies and redirect to the login page is not present.[Authorize(AuthenticationSchemes = "BasicAuthentication")]
tag will check the header and reply a 401 Unauthorized HTTP code if not present.[Authorize(AuthenticationSchemes = "BasicAuthentication,Cookies")]
tag will allow both methods to access the page, but somehow use the Cookies redirection mechanism when failing both checks.My goal is to have most of my project to use Cookies (hence why it is set as default), but have some API type of controllers to accept both methods. It should also be possible to tag the Controllers/Actions to return a specific Json body when desired (as opposed to the login redirect or base 401 response), but only for certain controllers.
I've spent the last 2 days reading different similar questions and answers here on StackOverflow, nothing seems to accommodate my need.
Here's a few methods I found:
AddCookie
allow you to set certain events, like OnRedirectToAccessDenied
and change the response from there. This does not work because it applies to the whole project.BasicAuthenticationHandler
class, the AuthenticationHandler
class allow to override HandleChallengeAsync
to change the response from there instead of replying 401. Unfortunately, again it applies globally to everywhere you use the scheme, not on a controller/action level. Not sure if it's applied when mixing multiple schemes either.AuthorizeAttribute, IAuthorizationFilter
. Again, this allow to override the OnAuthorization
method to decide if the user have the right or not to access the resource, but not to control the response AFTER the normal authentication scheme failed.I'm thinking either there's a filter type I'm missing, or maybe I need to create a third authentication type that will mix the previous two and control the response from there. Finding a way to add a custom error message in the options would also be nice.
Upvotes: 1
Views: 2559
Reputation: 728
I managed to do it via a IAuthorizationMiddlewareResultHandler
. Not my favorite approach because there can be only one per project and it intercepts all calls, but by checking if a specific (empty) attribute is set, I can control the flow:
public class JsonAuthorizationAttribute : Attribute
{
public string Message { get; set; }
}
public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler DefaultHandler = new AuthorizationMiddlewareResultHandler();
public async Task HandleAsync(RequestDelegate requestDelegate, HttpContext httpContext, AuthorizationPolicy authorizationPolicy, PolicyAuthorizationResult policyAuthorizationResult)
{
// if the authorization was forbidden and the resource had specific attribute, respond as json
if (policyAuthorizationResult.Forbidden)
{
var endpoint = httpContext.GetEndpoint();
var jsonHeader = endpoint?.Metadata?.GetMetadata<JsonAuthorizationAttribute>();
if (jsonHeader != null)
{
var message = "Invalid User Credentials";
if (!string.IsNullOrEmpty(jsonHeader.Message))
message = jsonHeader.Message;
httpContext.Response.StatusCode = 401;
httpContext.Response.ContentType = "application/json";
var jsonResponse = JsonSerializer.Serialize(new
{
error = message
});
await httpContext.Response.WriteAsync(jsonResponse);
return;
}
}
// Fallback to the default implementation.
await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
Upvotes: 1
Reputation: 1670
I was typing this on comment... but it's doesn't fit... so here is something we probably need to make clear before choosing a solution:
Yes, AuthorizationMiddleware
was registered when we use app.UseAuthorization();
, that quite far above controller layer, so it was returned long before the request can reach controller, so, any type of filter cannot be applied here.
Imagine, Authentication process return an instance of User
that stick with the request, but what would happen if the permission on cookie
and basicAuth
was difference, like cookie have myclaim
, while basicAuth
doens't ? Related process on both type of scheme was difference (like challenge on cookie
would lead to /Account/Login
and basicAuth
to /Login
?). And various logic case that we could implement on each page.
I Know, this is not possible, but it would become a mess, not for the author of these code, but for those maintainers to come.
This might sound detailed at first glance, but it would rather become burden soon, if some more authentication requirement raise after that (like Jwt). Covering each of these case on client would make user experience quite awkward (like, half-authentication and authorization).
And if It's un-avoidable in the project. Might I suggest create a default authentication scheme with ForwardDefaultSelector
that would elected which authentication scheme to use for each request. And maintain a stable routing HashSet
that would use to detect on which endpoint to set Json Response as wished on some upper level than AuthorizationMiddleware
, by using middleware, ofcourse. Then, we narrow down to 2 centralize places to checkout the authorization.
Chaos came when we tried to make one thing to do somethings. At least in this case, I think we would breath easier when coming to debug phase.
Upvotes: 0