Having issues with IAntiforgery in ASP.NET Core 6.0 - c#

Please look at my long answer at the end about how I resolved this. I had gotten too frustrated and after another day with a fresh perspective and more sleep, I got to a solution.
I did this in 5.0 with no issues in the Startup.Configure method.
Basically I created a header for the request on a protected route. I'm using React as the front end. I'm finding when I place everything in Program.cs the dependency injection, authorization doesn't work right so I split up into separate Program and Startup files.
But I can't use the following signature in 6.0 like I did in 5.0:
example that worked in 5.0:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("antiforgery/token", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(StatusCodes.Status200OK);
});
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
Program.cs (my attempt to split up program and startup - 6.0)
var startup = new dolpassword.Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
var app = builder.Build();
startup.Configure(app,app.Environment);
Saw this example on Microsoft website:
app.UseRouting();
app.UseAuthorization();
// app.Services syntax error in Configure for 6.0
var antiforgery = **app.Services.GetRequiredService<IAntiforgery>();**
app.Use((context, next) =>
{
var requestPath = context.Request.Path.Value;
if (string.Equals(requestPath, "/",
StringComparison.OrdinalIgnoreCase)
|| string.Equals(requestPath, "/index.html",
StringComparison.OrdinalIgnoreCase))
{
var tokenSet = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN",
tokenSet.RequestToken!,
new CookieOptions { HttpOnly = false });
}
return next(context);
});

I was able to successfully do this in 6.0 so I will share some of the code and how I resolved it. I also had Windows authentication baked in with a policy-based authorization. The reason I'm putting all the authentication/authorization wireup in this post is because the entire solution relies on authentication, authorization and antiforgery.
First I set up my services. I get IAntiforgery by default by adding ControllersWithViews but I want to use my own header name, which is X-XSRF-TOKEN instead of the .AspNet.Antiforgery.xxxx or whatever the default is. I also needed options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme; to get Windows auth working.
string CorsPolicy = "CorsPolicy";
//===================================formerly Configure Services
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
ConfigurationManager _configuration = builder.Configuration;
// Add services to the container.
**IServiceCollection? services = builder.Services;
services.AddAntiforgery(options => { options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.HttpOnly = false; });**
services.AddTransient<IActiveDirectoryUserService, ActiveDirectoryUserService>();
services.AddControllersWithViews();
services.AddAuthentication(options => {//needed for Windows authentication
options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme;
});
Adding more...
I'm using Windows auth so I'm using the Negotiate provider. Then I set up my Authorization. I insert my own authorization policy and also add my claims transformers to get the authenticated user into a claim. The fallback policy in Authorization was causing an Authentication exception.
services.AddAuthorization(options =>
{
// options.FallbackPolicy = options.DefaultPolicy;//authorization bombs if you include this line
options.AddPolicy("AuthenticatedOnly", policy => {
policy.Requirements.Add(new AuthenticatedRequirement(true));
});
});
services.AddTransient<IClaimsTransformation, MyClaimsTransformer>();
services.AddTransient<IAuthorizationHandler, AppUserRoleHandler>();
services.AddTransient<IAuthorizationHandler, AuthenticatedRoleHandler>();
services.AddCors(options =>
{
options.AddPolicy(CorsPolicy,
builder => builder
.WithOrigins("https://localhost:7021","https://localhost:44414")
//Note: The URL must be specified without a trailing slash (/).
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
Now I'm in the middleware territory...as you know, order matters in your middleware! In 5.0 you could add IAntiforgery to your constructor and DI would handle the rest. In program.cs you don't have that luxury. Fortunately you can just grab it out of your services collection and you see that in the following code.
//==============formerly Startup.Configure=====================
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseCors(CorsPolicy);
IAntiforgery? antiforgery = app.Services.GetRequiredService<IAntiforgery>();
Now when I'm setting up my endpoint routing. Found out that UseRouting and Use.Endpoints are married at the hip and need to be paired.
I also create a protected route "/auth" (protected by my authorization policy) to grab the antiforgery request token generated when we added it in the services collection. So this header won't be persisted from request to request like a cookie would. The minimal API allows me to create a route without creating the controller and action in a separate controller class.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/auth", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(Results.Ok());
}).RequireAuthorization("AuthenticatedOnly");
endpoints.MapControllerRoute(
name: "default"
pattern: "{controller}/{action=Index}/{id?}");
});
My React front end will use a fetch get request to get the token from the headers collection and then stick into a second post request and voila it works.
BTW, React doesn't provide Antiforgery functionality out of the box like Angular, in step with it's minimalist API ethos.
The action I'm posting to looks like this:
[HttpPost]
[Authorize(Policy="AuthenticatedOnly")]
[ValidateAntiForgeryToken]
public string Update()
I fully realize there are other ways to do this.

Related

How can I create a new instance of GraphServiceClient when my BootstrapContext is null .net6

I'm currently trying to get a token from the bootstrap context in a test application that I have put together to help understand a separate issue. This is the first time that I have configured a .net 6 web application (generally I work in Angular) authentication pipeline so I'm not sure if I am missing something simple.
The code that I have configured in program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// 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.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
When I access HttpContext.User.Identities.First() - I have access to a valid object and the user is shown as successfully authenticated. However, the BootstrapContext is always null...I did some digging and found that I might need to configure the auth service to save tokens, I tried that using the following alternative code but still no luck:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(x =>
{
var configurationSection = builder.Configuration.GetSection("AzureAd");
x.ClientId = configurationSection["ClientId"];
x.TenantId = configurationSection["TenantId"];
x.ClientSecret = configurationSection["ClientSecret"];
x.Domain = configurationSection["Domain"];
x.CallbackPath = configurationSection["CallbackPath"];
x.Instance = configurationSection["Instance"];
x.SaveTokens = true;
});
Does anyone have any ideas as to how I can get the BootstrapContext populated?
Alternatively, given the fact that I have an authenticated user, is there a way to create a new instance of GraphServiceClient using the HttpContext.User object? - that is ultimately what I'm trying to achieve here.

Trouble authorizing .NET 5 Web API reference tokens to IdentityServer3 using IdentityServer4.AccessTokenValidation package

Server
Using IdentityServer3 for client/application authorization.
Using IdentityAdmin to edit clients/scopes via GUI.
Created a new Client for the API, added a SharedSecret and api scope.
API / Client
Has 2 GET endpoints.
Uses the IdentityServer4.AccessTokenValidation NuGet package.
Configuration should be simple:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(c => {
var policy = ScopePolicy.Create("api");
c.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => {
options.Authority = "{base url of identity server}";
options.ApiName = ""; // not sure what this is? client id from identity server?
options.ApiSecret = ""; // should this be the hashed password?
options.LegacyAudienceValidation = true;
});
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MarvalAPI", Version = "v1" });
});
RegisterServices(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MarvalAPI v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); //this is added, everything else is by default
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
Testing:
GET client reference token from identity "/connect/token" endpoint
GET API's endpoint with added header "Authorization: Bearer {token}"
Receive 401 Unauthorized
Things I have tried:
Different Startup.cs configurations
Tried validating token via identity "/connect/accesstokenvalidation" endpoint, token is valid.
Different apiname/apisecret values, because not 100% sure what they have to be.
Googled to no avail
I am at a loss here, am I doing something totally wrong? Is this just a compatibility issue? Or am I just not understanding anything at all? Seems like clear documentation is scarce and users have to draw out information.
Sources used
https://github.com/IdentityServer/CrossVersionIntegrationTests/blob/main/src/CoreApiIdSrv3/Startup.cs
https://github.com/IdentityServer/IdentityServer4.AccessTokenValidation
IdentityServer3 documentation
SO / github/identityserver3 threads.
Well, some time after making this post I figured it out.
options.ApiName = "";
options.ApiSecret = "";
ApiName is the name of the scope which the client uses, so it this case the value should be api.
ApiSecret is the PRE-HASHED value of the scope secret.
e.g.
if secret value is "test" and it's SHA256 value is 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, then ApiSecret value should be test
So, after figuring this out, the above options config should look like this:
options.ApiName = "api";
options.ApiSecret = "test";
Note: SHA512 works as well.
To me this seems like a major naming issue.
I solved this after analysing this VS solution:
https://github.com/IdentityServer/CrossVersionIntegrationTests

How to check user-agent in ASP.NET Core health check calls when using own authentication, authorization?

I used the accepted answer to How to check user-agent in ASP.NET Core health check calls (MapHealthChecks)? , with one difference in requirement:
My application is not using App services authentication and authorization. Therefore, I needed to allow anonymous access for healthcheck as per documentation.
Here are changes to Startup.cs
//other services
services.AddHttpContextAccessor();
services.AddScoped<IAuthorizationHandler, UserAgentAuthorizationHandler>();
services.AddHealthChecks()
.AddCheck<HealthCheckFoo>("health_check_foo")
.AddCheck<HealthCheckBar>("health_check_bar");
//other services.AddAuthorization
services.AddAuthorization(options =>
{
options.AddPolicy("HealthCheckPolicy", builder =>
{
builder.AddRequirements(new UserAgentRequirement("HealthCheck/1.0"));
});
});
//...
app.UseEndpoints(endpoints =>
{
//other endpoints...
endpoints.MapHealthChecks("/health", new HealthCheckOptions { AllowCachingResponses = false })
.RequireAuthorization("HealthCheckPolicy");
.WithMetadata(new AllowAnonymousAttribute());
My expectation is that when testing locally, https://localhost:5001/health return an error. It does not.
It looks as your startup class has a mistake on the endpoints.MapHealthChecks adds a RequireAuthorization but as the same time you also add the AllowAnonymousAttribute.
Try with:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health", new HealthCheckOptions()
{
AllowCachingResponses = false,
})
.RequireAuthorization("HealthCheckPolicy");
});

How do I add AllowAnonymousAttribute to a route/request from custom midleware?

I have an ASP.NET Core website that is build with static files and ASP.NET Core routing middleware, so no MVC.
I requiring all request to be authenticated by default.
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
I have a mix of middlewares and routing configuration - but how do I specify that some routes of the static files and my custom middleware needs to Allow Anonymous?
Is there a way to app.Use(...) and tell the context that authorization has been applied and it should not use the fallback? So my custom middlewares can be set up correctly or am I required to set everything up with endpoint routing and added metadata to the routes?
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<NextJSMiddleware>();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseEndpoints(config =>
{
///This is for detecting whether the application process has crashed/deadlocked. If a liveness probe fails, app can be stopped/restarted, or create a new one.
config.MapHealthChecks("/.well-known/live", new HealthCheckOptions { Predicate = _ => false }).WithMetadata(new AllowAnonymousAttribute()); ;
///Readiness probe. This is for detecting whether the application is ready to handle requests.
config.MapHealthChecks("/.well-known/ready").WithMetadata(new AllowAnonymousAttribute());
config.MapEAVFrameworkRoutes();
config.MapPost("/.auth/login/passwordless", async (httpcontext) =>
{
...
}).WithMetadata(new AllowAnonymousAttribute());
config.MapGet("/.auth/login/passwordless/callback", async (httpcontext) =>
{
...
});
});

How can I exclude some routes from UseStatusCodePagesWithReExecute?

I have a full-stack app in ASP.NET Core 5. The front-end is React and the back-end is OData.
I need to use app.UseStatusCodePagesWithReExecute("/"); in my Configure() method to redirect any unknown requests to index.html, as routing is handled by the client-side code.
The problem is that in OData standard, when a key in a GET request is invalid, it returns a 404 error. This error will cause a redirect to index.html as well.
My Question: How can I exclude any request that starts with /odata.svc from UseStatusCodePagesWithReExecute()?
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// generated swagger json and swagger ui middleware
// You can access the swagger ui at /swagger/index.html
app.UseSwagger();
app.UseSwaggerUI(x => x.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Core Sign-up and Verification API"));
//app.UseCors("CorsPolicy");
// global cors policy
app.UseCors(x => x
.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
});
app.UseRouting();
// global error handler
app.UseMiddleware<ErrorHandlerMiddleware>();
// custom jwt auth middleware
app.UseMiddleware<JwtMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.Select().Expand().Filter().OrderBy().Count().MaxTop(30);
// add an endpoint for an actual domain model
// registered this endpoint with name odata (first parameter)
// and also with the same prefix (second parameter)
// in this route, we are returning an EDM Data Model
endpoints.MapODataRoute("odata.svc", "odata.svc", GetEdmModel(app.ApplicationServices));
endpoints.EnableDependencyInjection();
endpoints.MapControllers();
// enable serving static files
endpoints.MapDefaultControllerRoute();
});
// Redirects any unknown requests to index.html
app.UseStatusCodePagesWithReExecute("/");
app.UseHttpsRedirection();
// Serve default documents (i.e. index.html)
app.UseDefaultFiles();
//Set HTTP response headers
const string cacheMaxAge = "1";
// Serve static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// using Microsoft.AspNetCore.Http;
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={cacheMaxAge}");
}
});
}
This seems like an ideal use case for the UseWhen() extension method. This functionality is (under)documented by Microsoft, though #Paul Hiles has a more comprehensive write-up about it on his DevTrends blog. Basically, this allows you to conditionally inject middleware into your execution pipeline.
So, to conditionally exclude UseStatusCodePagesWithReExecute() if your request path starts with /odata.svc, you would simply wrap your UseStatusCodePagesWithReExecute() call with a UseWhen() condition, as follows:
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/odata.svc"),
appBuilder =>
{
appBuilder.UseStatusCodePagesWithReExecute("/");
}
);

Categories

Resources