IHttpContextAccessor session invalid operation exception in CustomClaimsTransformer - asp.net-core

I followed the official docs to access the HttpContext from custom components in a CustomClaimsTransformer.
I previously had a .NET Core web application, where I used the session to enable admins to jump into the application with the view (identity) of a different user (for support purposes). I store the information, for which user the view should be prepared in the Session. Now I wanted to make it more elegant and use the .net core authorization using claims and role based authorization. As there is a Windows authentication behind, I’ve to use the CustomClaimsTransformer. Now my problem is, I want to access the current session from within the CustomClaimsTransformer. I can inject the IHttpContextAccessor, but IHttpContextAccessor.Session always raises an invalid operation exception.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/Account/Login/");
options.AccessDeniedPath = new PathString("/Account/Forbidden/");
});
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole", policy => policy.RequireClaim(ClaimTypes.Role, "admin"));
options.AddPolicy("Test1", policy => policy.RequireClaim("Rechte", " Test1"));
options.AddPolicy("Test2", policy => policy.RequireClaim("Rechte", " Test2"));
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
//services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpContextAccessor();
services.AddTransient<IClaimsTransformation, CustomClaimsTransformer>();
services.AddDistributedMemoryCache();
services.AddSession();
}
CustomClaimsTransformer:
CustomClaimsTransformer:
public class CustomClaimsTransformer : IClaimsTransformation
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomClaimsTransformer(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var z = _httpContextAccessor.HttpContext; //works
var zz = _httpContextAccessor.HttpContext.Session; // System.InvalidOperationException: "Session has not been configured for this application or request."
I edited my ConfigureServices above, when pasting the code I removed some lines for readability, including the AddDistributedMemoryCache line, sorry. Session is working in the app except where shown.
Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

Reference Session and app state in ASP.NET Core
To enable the session middleware, Startup must contain:
Any of the IDistributedCache memory caches. The IDistributedCache implementation is used as a backing store for
session.
A call to AddSession in ConfigureServices.
A call to UseSession in Configure.
Also
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
//... removed for brevity
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession(); // This must come before "UseMvc()"
app.UseHttpContextItemsMiddleware();
app.UseMvc();
}
The order of middleware is important. In the preceding example, an
InvalidOperationException exception occurs when UseSession is
invoked after UseMvc.
...
HttpContext.Session can't be accessed before UseSession has been called.

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.'

User assigned roles remain unrecognised

I am trying to introduce an admin account into my project. I have updated the Startup.cs file to use roles and have added Admin and Standard account inside AspNetRoles and assigned a user using their UserID an admin. The relevant [Authorize(Role = "Admin")] has also been added to the page but the admin account remains denied access. I can't seem to find out what is causing this to not be recognised, I have searched similar posts which suggested implementing
.AddRoleManager<RoleManager<IdentityRole>>()
However, this did not seem to help either. Would appreciate any insight in resolving this issue. Using Blazor wasm. Thanks in advance.
Startup.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)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
services.AddControllers().AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize);
}
// 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.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();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
AspNetRoles table
AspNetUserRoles table
Razor pages
#attribute [Authorize(Roles ="Admin")]
Despite the above implementations, when logging into the assigned Admin account, the message shown is
You are not authorized to access this resource.
Try the following...
Change:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
To:
// Configure identity server to put the role claim into the id token
// and the access token and prevent the default mapping for roles in
// the JwtSecurityTokenHandler.
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");

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

Authorize Attribute Still Passes As Successful Even If User Isn't Logged In

Good Day,
I'm stumped as to why even though I use the [Authorize] attribute on my controllers, it doesn't check to see if a user is logged in and still passes as an Authorization success. I am following the basic Identity and Authorization tutorials from Microsoft, HERE and HERE. I was able to get basic authentication up, creating a user and logging in and all that, but the authorization just allows guest to pass through and the system falsely recognizes them as successful. I used chrome to test, so i even used private mode and cleared the cookies and cache in the event the information was stored. I'm completely stumped, and I don't know what else to do.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization was successful.
Is the Authorization success message I get in the Debug console log.
Below is Startup.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.
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.AddRazorPages();
services.AddControllersWithViews();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});
services.AddDbContext<DevContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<UserContext>(options => options.UseSqlServer(Configuration.GetConnectionString("UserContextConnection")));
services.AddIdentity<User, IdentityRole>().AddEntityFrameworkStores<UserContext>().AddDefaultTokenProviders();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.ConfigureApplicationCookie(options =>
{
//Cokie Settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromDays(150);
//If the LoginPath isn't set, ASP.NET Core defaults the path to Account/Login.
// options.LoginPath = "/Account/Login";
// options.AccessDeniedPath = "/Account/AccessDenied";
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});
// services.AddSingleton<IEmailSender, EmailSender> ();
}
// 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.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapDefaultControllerRoute().RequireAuthorization();
}
);
}
}
Below is the User.cs, left it blank because the basic demo didn't have any custom fields and it still worked. So I wasn't sure that would be the issue.
public class User : IdentityUser
{
}
And this is the Home Controller with the [Authorize] attributes
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize]
public IActionResult Information()
{
ViewData["Message"] = "Test Information Page";
return View();
}
[Authorize]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
[Authorize]
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
[Authorize]
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
I think your problem is this line:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
This means your application will use your windows login to authenticate you and not the cookie you created.
I would change it to this since you are using a cookie based authentication scheme:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
See guide below:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.0
I would also add functionality for creating and handling an antiforgery token to secure your application against cross-forgery.
Update (Solution):
This implementation is usign Identity which is already added so no need to call AddAuthentication()
Similar issue to this: github.com/aspnet/AspNetCore/issues/4656

Adding super user with admin role in .net core framework

currently I'm trying to add a super User with Admin role in ASP.Net Core web app.
I want to add this user at startup, I spend some time researching about the subject without any success.
The sartup looks very standard and out of the box as follows
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets();
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseIdentity();
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
How this can be achieved?
Add parameter ApplicationDbContext dbContext into Configure method - dependency injection will create appropriate object and you can Find/Add required users:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, ApplicationDbContext dbContext)
{
...
if (dbContext.Users.Find(x => Name == "superadmin") == null)
{
db.Users.Add(new User { Name = "superadmin", ... });
db.SaveChanges();
}
}
Or you may add UserManager into parameters and use it for users manipulation.