How to configure dotnetcore 3 site to return 401 with custom authorization filter rather than redirecting to AccessDenied page - asp.net-core

I've got a site using the new dotnetcore3 angular template. I've created a custom authorization filter that's currently extremely simple
public class ClaimRequirementFilter : IAuthorizationFilter
{
readonly string _claim;
public ClaimRequirementFilter(string claim)
{
_claim = claim;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_claim != "test")
{
context.Result = new ForbidResult();
}
}
}
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(string claimType) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] {claimType };
}
}
[Route("{jobId}")]
[ClaimRequirement("testfail")]
[HttpGet]
public async Task<IActionResult> GetJob([FromRoute] Guid jobId)
{
//stuff
{
However, whenever a request fails (which will be all of them right now), it 302s me to the AccessDenied page with a returnUrl of the URL I was trying to hit.
However, since this request is being made from my angular client, I would rather it just return a 401 (or 403 since in this case it's because the loggedin user doesn't have permission to do what they're trying to do), and I'm not sure how to configure it.
Per Ruard's request, here is my Startup configuration
public class Startup
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);
services.AddTransient<EmailSender, EmailSender>();
services.AddScoped<IRazorViewToStringRenderer, RazorViewToStringRenderer>();
services.Configure<EmailServiceConfiguration>(Configuration.GetSection("EmailServiceConfiguration"));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/auth/login";
options.UserInteraction.LogoutUrl = "/auth/logout";
})
//.AddDeveloperSigningCredential()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
// .AddGoogle(options =>
// {
// IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google");
// options.ClientId = googleAuthNSection["ClientId"];
// options.ClientSecret = googleAuthNSection["ClientSecret"];
// })
.AddIdentityServerJwt();
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddRazorPages(options =>
{
options.Conventions.AddAreaPageRoute("Identity", "/Identity/Account/Login", "/auth/login");
});
services.AddAuthorization(options =>
{
// options.AddPolicy("RequireAdmin", policy =>
// {
// policy.RequireRole("Admin");
// });
// options.AddPolicy("CreateInternalUsers", policy =>
// {
// // policy.RequireRole("Admin");
// policy.RequireClaim("CreatePPGUser");
// });
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ApplicationDbContext context, IServiceProvider services)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/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();
}
context.Database.Migrate();
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}"
);
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
// spa.UseAngularCliServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
}
});
CreateUserRoles(services).Wait();
}
}

You should add a controller that serves as api. From the documentation:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
Methods from this controller will return the StatusCode instead of redirecting the user to a view.
Update
Based on your startup, it seems that you are not setting the CompatibilityVersion:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
As documented, this is required in combination with ApiController:
The preceding change:
Is required to use the [ApiController] attribute at the controller level.
Opts in to potentially breaking behaviors introduced in ASP.NET Core 2.2.
In your case, assuming version 3.0:
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

Related

ASP.NET Core Web Api - Problem with using session

I am developing an asp.net core web api and I want to store a token that will be sent to my endpoints in order for a user to authenticate. For that I have some requirements which force me to implement an own authentication method. I inherit from AuthenticationHandler and implement the HandleAuthenticateAsync method:
public AuthenticateHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IHttpContextAccessor httpContextAccessor)
: base(options, logger, encoder, clock)
{
AuthenticateHandlerHelperFunctions = new AuthenticateHandlerHelperFunctions();
_checkAccessTokenModel = new CheckAccessTokenModel();
session = httpContextAccessor.HttpContext.Session;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//before this: check header and get authorization informations
string submittedToken = authorizationheader.Substring("bearer".Length).Trim();
try
{
if (string.IsNullOrEmpty(session.GetString("Token")))
{
_checkAccessTokenModel = await AuthenticateHandlerHelperFunctions.CheckAccessToken(submittedToken);
if(_checkAccessTokenModel.Active == false)
{
_failReason = "Token not valid anymore, request another one!";
return AuthenticateResult.Fail("Token not valid anymore, request another one!");
}
session.SetString("Token", submittedToken);
}
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
var claims = new[] {
new Claim(ClaimTypes.Name, _checkAccessTokenModel.Exp.ToString()),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
The goal is to use the session to save the token and not execute the CheckAccessToken method for every request. I will get frequent data on the endpoints that are configured with [Authorize] so I want to save computing time. I looked this up and most of the errors where problems with the startup where the app.UseSession() was not set correctly etc. but not in my case I believe. Here is my Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "DigVPapi", Version = "v1" });
});
services.AddDbContextFactory<AntragDBNoInheritanceContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, AuthenticateHandler>("BasicAuthentication", null);
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = System.TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddHttpContextAccessor();
services.AddSingleton<IJWTManagerRepository, JWTManagerRepository>();
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.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "DigVPapi v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
If this is not possible. What could I do instead to save the token in some different way? Of course I could save the token in the database but this would only move the problem to a database query tha twould be made every time. The error that I get when trying to authenticate is following
System.InvalidOperationException: 'Session has not been configured for this application or request.'

Blazor WASM Hosted - Authorize on API Always returns UnAuthorized

I have a blazor wasm hosted solution that is setup using Role authentication. However, whenever I add an [Authorize] attribute to any of my API Controllers I get a 401 Unauthorized. I know the user has the proper role as the UI is showing and hiding features for that role. Its like the roles are not being passed up to the API. What am I missing?
Server - Starup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//Register the Datacontext and Connection String
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
//Sets up the default Asp.net core Identity Screens - Use Identity Scaffolding to override defaults
services.AddDefaultIdentity<ApplicationUser>( options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<DataContext>();
//Associates the User to Context with Identity
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, DataContext>( options =>
{
options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Role);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove(JwtClaimTypes.Role);
//Adds authentication handler
services.AddAuthentication().AddIdentityServerJwt();
//Register Repositories for Dependency Injection
services.AddScoped<ICountryRepository, CountryRepository>();
services.AddControllersWithViews();
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/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();
}
//AutoMigrates data
dataContext.Database.Migrate();
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseSerilogIngestion();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
Client - Program.cs
public class Program
{
public static async Task Main(string[] args)
{
//Serilog
var levelSwitch = new LoggingLevelSwitch();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.Enrich.WithProperty("InstanceId", Guid.NewGuid().ToString("n"))
.WriteTo.BrowserHttp(controlLevelSwitch: levelSwitch)
.CreateLogger();
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("XXX.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("XXX.ServerAPI"));
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
var baseAddress = new Uri($"{builder.HostEnvironment.BaseAddress}api/");
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(client =>
{
client.BaseAddress = apiBaseUrl;
});
}
RegisterTypedClient<ICountryService, CountryService>(baseAddress);
await builder.Build().RunAsync();
}
}
RolesClaimPrincipalFactory.cs
public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
ClaimsPrincipal user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
Claim[] roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
if (roleClaims != null && roleClaims.Any())
{
foreach (Claim existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
}
You are likely having this issue since you are using ICountryService that has it's own http client which is not configured to include auth tokens in the outgoing requests -- no tokens, no access.
We can attach tokens by adding an AuthorizationMessageHandler to the client, just like your named client (XXX.ServerAPI) is configured.
Try changing your typed client helper method to this:
/* Client Program.cs */
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(
client => client.BaseAddress = apiBaseUrl)
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
}
You probably want to change the helper to also only include tokens to client's that actually require them (if you are using that helper for other clients as well)
See the docs for more info.

Asp.Net CORS security issue

I made a CORS policy in my asp.net core API, but it seems the API is accessible for the public, as I can see the JSON by just typing the API URL in browser. So I just want my front-end(which is a angular app in the same host of API) have access to API not everyone.
this is my start up class:
public class Startup
{
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("myurl")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.DictionaryKeyPolicy = null;
});
services.Configure<FormOptions>(o =>
{
o.ValueLengthLimit = int.MaxValue;
o.MultipartBodyLengthLimit = int.MaxValue;
o.MemoryBufferThreshold = int.MaxValue;
});
services.AddControllers();
string connectionString = "xxx";
services.AddDbContext<decorContext>(
option => option.UseSqlServer(connectionString)
);
}
// 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.UseDefaultFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory() ,#"StaticFiles")),
RequestPath = new PathString("/StaticFiles")
});
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
CORS is kicked in when Domain A accesses Domain B (like making an ajax call in js) and it won't apply when you hit the url directly. That's why it's called CROSS-ORIGIN.
You don't typically implement "script -> website" restriction. You could restrict access by location/IP, user identity/role/claims, or by checking if antiforgery token is present in a request. Check out Overview of ASP.NET Core Security

Shared resource localization is not working

I am working on shared resource localization in Asp .Net Core 3.1. For that I created resource directory and created SharedResources.cs file.
Root/Resources/SharedResources.cs
Root/Resources/SharedResources.en.resx
I injected code in controller.
public AccountController(IStringLocalizer<SharedResources> sharedLocalizer)
{
_sharedLocalizer = sharedLocalizer;
}
public IActionResult Login(LoginViewModel model)
{
if(loginSuccess == true)
{
return RedirectToAction("Dashboard", "Dashboard");
}
TempData["Error"] = _sharedLocalizer["Error"];
return View(model);
In SharedResources.en.resx
Key : Error
Value : Invalid User
In SharedResources.cs
namespace RootName
public class SharedResources
{
}
It displays Error and it should display Invalid User. Where am I wrong?
For asp.net core 3.x,you need to create SharedResources.cs in your root folder and create SharedResources.en.resx in root/Resources folder like below:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddLocalization(o =>
{
// We will put our translations in a folder called Resources
o.ResourcesPath = "Resources";
});
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo("en"),
new CultureInfo("de"),
};
options.DefaultRequestCulture = new RequestCulture("en");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
}
// 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();
}
else
{
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();
var localizationOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>().Value;
app.UseRequestLocalization(localizationOptions);
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

AuthorizationAttribute in ASP.NET Core 2.0

I am trying to configure cookie authentication with identity and ef. So far I am able to have a valid Set-Cookie in my Controller response. The browser sends this cookie back, but the AuthorizeFilter always redirects to Login page, thus Authentication doesn't seem to work. What am I suppoed to configure?
Here's my ConfigureServices in Startup so far:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddCors(o => o.AddPolicy("Cors", builder =>
{
builder.WithOrigins(Configuration["AllowedOrigins"].Split(","))
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader();
}));
services.AddDbContext<MyIdentityDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<MyIdentityDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => {
if (!String.IsNullOrEmpty(Configuration["AuthCookieDomain"]))
{
options.Cookie.Domain = Configuration["AuthCookieDomain"];
}
options.Cookie.Name = Configuration["AuthCookieName"];
options.Cookie.HttpOnly = false;
options.Cookie.SameSite = SameSiteMode.None;
});
}
Then, my Configure in Startup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
IServiceProvider serviceProvider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("Cors");
app.UseMvc();
app.UseAuthentication();
}
Then, my Action that actually successfully sets the cookie
// GET api/values
[HttpPost]
public async Task<ActionResult> Post([FromBody] AuthPost post)
{
if (post == null || String.IsNullOrEmpty(post.UserName) || String.IsNullOrEmpty(post.Password))
{
return BadRequest();
}
var result = await signInManager.PasswordSignInAsync(post.UserName, post.Password, true, false);
if (result.Succeeded)
{
return Ok();
}
return Unauthorized();
}
And finally, my other Action with Authorize attribute that does not work (Always redirects to Login)
[HttpGet]
[Authorize]
public async Task<ActionResult> Get()
{
var user = await userManager.GetUserAsync(User);
return Ok(new { UserName = user.UserName });
}
OK, ConfigureApplicationCookie is the way to work. What caused the issue was the WRONG order of app.UseMvc(); and app.UseAuthentication();
app.UseAuthentication() must be invoked before app.UseMvc()!