.NET Core (.NET 5) HandleChallengeAsync in custom authentication - asp.net-core

I'm trying to write a custom authentication handler in a .NET 5 web app. The problem I'm unable to solve is how to handle redirection in HandleChallengeAsync. I see there's a RedirectUri property in the AuthenticationProperties parameter, but I don't know how and where to set it up, and once the value is set, do I need to issue a command to redirect to login URL or will it happen when calling base HandleChallengeAsync method?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("TeamAuth")
.AddScheme<AuthenticationSchemeOptions, NewTeamAuthHandler>("TeamAuth", null);
// ...
}
handler code
public class NewTeamAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public NewTeamAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
return AuthenticateResult.Fail("authentication faiure");
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
// probable location of the redirection command
await base.HandleChallengeAsync(properties);
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
}

Related

Differ IOutputFormatter per endpoint in ASP.NET Core 6

I have a legacy ASP.NET Web API 2 app which must be ported to ASP.NET Core 6 and it has the following behaviour:
Some controllers return responses in Pascal-case Json
Some controllers return responses in camel-case Json
All controllers have the same authentication/authorization, but they return different objects using different serializers for 401/403 cases.
In ASP.NET Web API 2 it was easily solved with IControllerConfiguration (to set the formatter for a controller), AuthorizeAttribute (to throw exceptions for 401/403), ExceptionFilterAttribute to set 401/403 status code and response which will be serialized using correct formatter.
In ASP.NET Core, it seems that IOutputFormatter collection is global for all controllers and it is not available during UseAuthentication + UseAuthorization pipeline where it terminates in case of failure.
Best I could come up with is to always "succeed" in authentication / authorization with some failing flag in claims and add IActionFilter as first filter checking those flags, but it looks very hacky.
Is there some better approach?
Update1:
Implementing different output formatters for IActionResult from controller or IFilter (including IExceptionFilter) is not very difficult.
What I want is to be able to either set IActionResult or use IOutputFormatter related to Action identified by UseRouting for Authentication/Authorization error or IAuthorizationHandler, but looks like all those auth steps are invoked before either ActionContext or IOutputFormatter is invoked.
So 2 approaches I see now:
hack auth code to "always pass" and handle HttpContext.Items["MyRealAuthResult"] object in IActionFilter
expose V1OutputFormatter/V2OutputFormatter in a static field and duplicate selection logic in HandleChallengeAsync/HandleForbiddenAsync based on to what controller/action it was routed from UseRouting step.
Here is sample app that uses auth and has 2 endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
builder.Services.AddAuthentication(options =>
{
options.AddScheme<DefAuthHandler>("defscheme", "defscheme");
});
builder.Services.AddAuthorization(options =>
options.DefaultPolicy = new AuthorizationPolicyBuilder("defscheme")
.RequireAssertion(context =>
// false here should result in Pascal case POCO for WeatherForecastV1Controller
// and camel case POCO for WeatherForecastV2Controller
context.User.Identities.Any(c => c.AuthenticationType == "secretheader"))
.Build())
.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultHandler>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
public class AuthorizationResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultHandler()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
// Can't set ActionContext.Response here or use IOutputFormatter
await _handler.HandleAsync(next, context, policy, authorizeResult);
}
}
public class DefAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DefAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<ClaimsIdentity>();
if (Request.Headers.ContainsKey("secretheader")) claims.Add(new ClaimsIdentity("secretheader"));
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claims), "defscheme"));
}
}
public class MvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ArrayPool<char> arrayPool;
private readonly MvcNewtonsoftJsonOptions mvcNewtonsoftJsonOptions;
public MvcOptionsSetup(ArrayPool<char> arrayPool, IOptions<MvcNewtonsoftJsonOptions> mvcNewtonsoftJsonOptions)
{
this.arrayPool = arrayPool;
this.mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions.Value;
}
public void Configure(MvcOptions options)
{
options.OutputFormatters.Insert(0, new V1OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
options.OutputFormatters.Insert(0, new V2OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
}
}
public class V1OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V1OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV1";
}
}
public class V2OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V2OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV2";
}
}
[ApiController]
[Authorize]
[Route("v1/weatherforecast")]
public class WeatherForecastV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be Pascal case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
[ApiController]
[Authorize]
[Route("v2/weatherforecast")]
public class WeatherForecastV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be camel case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
If there is no way to configure controllers independently, then you could use some middleware to convert output from selected controllers that meet a path-based predicate.
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapWhen(ctx => ctx.Request.Path.Containes("v2/"), cfg =>
{
app.UseMiddleware<JsonCapitalizer>();
});
app.Run();
And then create a JsonCapitalizer class to convert output from any path that contains "v2/". Note, this middleware will not run if the predicate in MapWhen is not satisfied.
public class JsonCapitalizer
{
readonly RequestDelegate _nextRequestDelegate;
public RequestLoggingMiddleware(
RequestDelegate nextRequestDelegate)
{
_nextRequestDelegate = nextRequestDelegate;
}
public async Task Invoke(HttpContext httpContext)
{
await _nextRequestDelegate(httpContext);
// Get the httpContext.Response
// Capitalize it
// Rewrite the response
}
}
There may be better ways, but that's the first that comes to mind.
The following link will help with manipulation of the response body:
https://itecnote.com/tecnote/c-how-to-read-asp-net-core-response-body/
I also faced such a problem in ASP Core 7 and ended up with writing an attribute.
So the attribute will be applied on each Action where the response type has to be converted. You can write many an attribute for camelcase response and another attribute for pascalcase. The attribute will look like below for CamelCase
public class CamelCaseAttribute : ActionFilterAttribute
{
private static readonly SystemTextJsonOutputFormatter formatter = new SystemTextJsonOutputFormatter(new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Formatters
.RemoveType<NewtonsoftJsonOutputFormatter>();
objectResult.Formatters.Add(formatter);
}
else
{
base.OnActionExecuted(context);
}
}
}
And on the Contoller Action you can use it like below
[CamelCase]
public async IAsyncEnumerable<ResponseResult<IReadOnlyList<VendorBalanceReportDto>>> VendorBalanceReport([FromQuery] Paginator paginator, [FromQuery] VendorBalanceReportFilter filter, [EnumeratorCancellation] CancellationToken token)
{
var response = _reportService.VendorBalanceReport(paginator, filter, token);
await foreach (var emailMessage in response)
{
yield return emailMessage;
}
}

how to set the output type List<string> in a middleware in .NET Core 2.1?

I have this middleware class when I want to show a List<string> in the output:
namespace WebAspNetCore2_1
{
public class LearningMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LearningMiddleware> _logger_log;
private readonly List<string> logger;
public LearningMiddleware(RequestDelegate next,ILogger<LearningMiddleware> logger_log)
{
_next = next;
_logger_log = logger_log;
List<string> _logger = new List<string>
{
("EUR/USD"),
("1.0500")
};
logger = _logger;
}
public Task Invoke(HttpContext httpContext)
{
_logger_log.Log(Microsoft.Extensions .Logging.LogLevel.Information,"information of logger",logger[0]);
return _next(httpContext);
}
}
}
I have debugged my code but seen to be correct, my List<> is filled, I don't know why the compiler is throwing this exception:
InvalidOperationException: Could not create an instance of type Microsoft.Extensions.Logging.ILogger`1[[System.String, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the 'logger' parameter a non-null default value.
i thought was the order declaration in StartUp, but not
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// app.UseLearningMiddleware();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMiddleware<LearningMiddleware>();
app.UseMvc();
}
link in video for detail evidence: https://youtu.be/2FoLvhLweYo
I tested your code in my side but it worked well... I created a new asp.net core 2.1 MVC project and create a middleware. In StartUp.cs, I put app.UseMiddleware<MyMiddleware>(); just before app.UseMvc(routes =>
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebApplication2
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<MyMiddleware> _logger_log;
private readonly List<string> logger;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger_log)
{
_next = next;
_logger_log = logger_log;
List<string> _logger = new List<string>
{
("EUR/USD"),
("1.0500")
};
logger = _logger;
}
public Task Invoke(HttpContext httpContext)
{
_logger_log.Log(Microsoft.Extensions.Logging.LogLevel.Information, "information of logger", logger[0]);
return _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
}

AuthenticationHandler ending request prematurely

I'm trying to make a .net core AuthenticationHandler with some custom logic. Whenever I make a request to the page everything in the auth handler runs fine but it returns a 200 without actually executing the code for my end controller. I've distilled it down to this simplified version.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Dummy")
.AddScheme<AuthenticationSchemeOptions, DummyAuthHandler>("Dummy", null);
...
My handler:
public class DummyAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DummyAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) :
base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
=> Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(), "Dummy")));
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
=> Task.CompletedTask;
}
I'm thinking I'm missing one of the methods needed to tell the framework to continue processing the request, and not to just think my authentication handler wants the page redirected. Maybe I even need to add a call to next() somewhere?
Cracked it thanks to this and this... the ClaimsIdentity was returning IsAuthenticated = False, which is why I thought I needed the HandleChallengeAsync in the first place. Here's what the fixed handler looks like:
public class DummyAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DummyAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) :
base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
=> Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(
new ClaimsIdentity("abc")), "Dummy")));
}

Validating AntiForgery token globally in ASP.NET Core

I want to validate AntiForgery token in ASP.NET Core application. I know i can individually do that by adding [AutoValidateAntiforgeryToken] or [ValidateAntiforgeryToken] attributes on Action methods as suggested in SO post here
I'm looking for global approach to validate token for all POST methods. So i created a middleware to do so. However i could not find suitable method to validate the token. Like in classic asp.net there is AntiForgery.Validate().
What's the equivalent method in ASP.NET Core
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate _next;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Method.ToUpper() == "POST")
{
// where does this mehod exists?
// i could not find it in Microsoft.AspNetCore.Antiforgery namespace
AntiForgery.Validate();
}
await _next(httpContext);
}
}
public static class ValidateAntiForgeryTokenMiddlewareExtensions
{
public static IApplicationBuilder UseValidateAntiForgeryToken(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
}
}
I have to Inject Antiforgery as service
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Method.ToUpper() == "POST")
{
await _antiforgery.ValidateRequestAsync(httpContext);
}
await _next(httpContext);
}
}
add Antiforgery as service in startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery();
}
Use my middlware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
{
app.UseValidateAntiForgeryToken();
}

Middleware check the annotation from controller method

Hello i want to check the annotation from a controller method in a middleware class.
My config:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, BackendDbContext context)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseStaticFiles();
app.UseMiddleware<AuthMiddleware>();
app.UseMvc();
BackendDbInitializer.Init(context);
}
My Controller:
Route("api/[controller]")]
public class UserController : Controller
{
private readonly BackendDbContext _context;
public UserController(BackendDbContext context)
{
_context = context;
}
// GET api/values
[HttpGet]
[NoAuth]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
My Middleware:
public class AuthMiddleware
{
private readonly RequestDelegate _next;
public AuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
//Here i want to check if the called method in UserController have a annotation...
await _next.Invoke(context);
}
}
In the AuthMiddleware i want to check if the called method have a specific annotation.
I don't know if this question is outdated, I came across the same question today, I'm using Casbin.Net package to implement RBAC for my asp dotnet core project, I need to implement an auth middleware which can recognize controllers with a [Authorize] annotation, so only these controllers need to check permission, the other controllers not, below are my code
rbac with asp dotnet core using casbin
You can ignore the dependency inject and casbin parts, in AuthzMiddleware.cs, I use context.User.Claims.Count() > 0 to check if the current request has passed a Authentication middleware.