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) =>
{
...
});
});
Related
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.
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.
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");
});
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("/");
}
);
I'm trying OpenIddict 3.0. I followed the steps in the documentation, created an Authorize controller, and added a test application. When I try to run I get this exception:
The OpenIddict ASP.NET Core server cannot be used as the default
scheme handler. Make sure that neither DefaultAuthenticateScheme,
DefaultChallengeScheme, DefaultForbidScheme, DefaultSignInScheme,
DefaultSignOutScheme nor DefaultScheme point to an instance of the
OpenIddict ASP.NET Core server handler
I cannot find what I'm doing wrong.
Here is my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
{
// Configure the context to use Microsoft SQL Server.
options.UseInMemoryDatabase("Identity");
// Register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict<Guid>();
});
AddIdentityCoreServices(services);
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>()
.ReplaceDefaultEntities<Guid>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint (required to use the password flow).
options.SetTokenEndpointUris("/connect/token");
// Allow client applications to use the grant_type=password flow.
options.AllowPasswordFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
//options.RegisterScopes(OpenIddictConstants.Scopes.Email,
// OpenIddictConstants.Scopes.Profile,
// OpenIddictConstants.Scopes.Roles);
// Accept requests sent by unknown clients (i.e that don't send a client_id).
// When this option is not used, a client registration must be
// created for each client using IOpenIddictApplicationManager.
options.AcceptAnonymousClients();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough() // Add this line.
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement.
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// ASP.NET Core Identity should use the same claim names as OpenIddict
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme;
});
services.AddControllers();
}
// 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();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private static void AddIdentityCoreServices(IServiceCollection services)
{
var builder = services.AddIdentityCore<ApplicationUser>();
builder = new IdentityBuilder(
builder.UserType,
typeof(ApplicationRole),
builder.Services);
builder.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddSignInManager<SignInManager<ApplicationUser>>();
}
Please assist me on what I'm doing wrong.
I finally figured out where I went wrong. #Train Thanks for pointing me in the right direction.
changing the services.AddAuthentication(...) from
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme;
});
to
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
What's you're method of authentication? Cookie? JWT?
You need to change this line of code. You can't set OpenIddictServerAspNetCoreDefaults.AuthenticationScheme; as the default scheme
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme;
});
Default Authentication Scheme
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
or overload
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
Here are the docs on Authentication with a lot more to read up on.