Make Identity core options configurable with database - c#

I am new to aspnet core. we are using identity core 2.1. Now made a page from where admin can set the different configuration like idle-time lockout-time password-retries. Those settings are being saved into the database table. Now I want that my identity option will set from those values. I made a repository to get setting from database. but I am unable to call that repository function from startup.cs.
Can some please guide me? and Also tell me the best way to make identity options configurable from database.
I have made a service
public class SecuritySettingService : ISecuritySettingService
{
private readonly ISecuritySettingRepository _securitySettingRepository;
public SecuritySettingService(ISecuritySettingRepository securitySettingRepository)
{
_securitySettingRepository = securitySettingRepository;
}
public SecuritySetting GetSecuritySetting()
{
return _securitySettingRepository.GetSecuritySetting();
}
}
A repository to connect to database
public class SecuritySettingRepository : ISecuritySettingRepository
{
private readonly IDbRepository _dapperWrapper;
public SecuritySettingRepository(IDbRepository dapperWrapper)
{
_dapperWrapper = dapperWrapper;
}
public SecuritySetting GetSecuritySetting()
{
var response = _dapperWrapper.QuerySingleOrDefault<SecuritySetting>("security_setting_get", null, CommandType.StoredProcedure);
return response;
}
}
Made identity config class to clean up startup.cs
public static class IdentityConfig
{
public static void ConfigureIdentity(IServiceCollection services, ISecuritySettingService securitySettingService)
{
var securitySetting = securitySettingService.GetSecuritySetting();
services.AddIdentity<ApplicationUser, ApplicationRole>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
}).AddUserManager<CustomUserManager>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = true;
});
services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromDays(30);
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.Cookie = new CookieBuilder
{
IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy
};
});
}
}
startup.cs file is like
public class Startup
{
public ISecuritySettingService _securitySettingService;
public Startup(IConfiguration configuration, ISecuritySettingService securitySettingService)
{
Configuration = configuration;
_securitySettingService = securitySettingService;
}
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)
{
IdentityConfig.ConfigureIdentity(services, _securitySettingService);
services.AddOptions();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
services.AddHttpContextAccessor();
}
// 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)
{
}
}
in startup.cs when I call IdentityConfig.ConfigureIdentity(services, _securitySettingService); the object _securitySettingService is not present so my code throws exception invalid operation

For your current error, you did you register ISecuritySettingService and used it in Startup. For referencing ISecuritySettingService, you need to register it first.
For services.Configure<IdentityOptions>, it will not change automatically while changing the database. You need to update IdentityOptions by yourself.
Follow Steps below and modify it as needed.
ISecuritySettingRepository
public interface ISecuritySettingRepository
{
LockoutOption GetSecuritySetting();
LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption);
}
SecuritySettingRepository
public class SecuritySettingRepository : ISecuritySettingRepository
{
private readonly DbConnection _dapperWrapper;
private readonly IConfiguration _configuration;
public SecuritySettingRepository(DbConnection dapperWrapper
, IConfiguration configuration)
{
_dapperWrapper = dapperWrapper;
_configuration = configuration;
}
public LockoutOption GetSecuritySetting()
{
using (var connection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection")))
{
string sQuery = "SELECT top 1 * From LockoutOption Where Id = 1";
var response = connection.QueryFirstOrDefault<LockoutOption>(sQuery);
return response;
}
}
public LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption)
{
using (var connection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection")))
{
string sQuery = $"Update LockoutOption Set MaxFailedAccessAttempts = {lockoutOption.MaxFailedAccessAttempts} Where Id = {lockoutOption.Id}";
var result = connection.Execute(sQuery);
string sQuery1 = "SELECT top 1 * From LockoutOption Where Id = 1";
var response = connection.QueryFirstOrDefault<LockoutOption>(sQuery1);
return response;
}
}
}
ISecuritySettingService
public interface ISecuritySettingService
{
LockoutOption GetSecuritySetting();
LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption);
}
SecuritySettingService
public class SecuritySettingService : ISecuritySettingService
{
private readonly ISecuritySettingRepository _securitySettingRepository;
private readonly IdentityOptions _identityOptions;
public SecuritySettingService(ISecuritySettingRepository securitySettingRepository
, IOptions<IdentityOptions> identityOptions)
{
_securitySettingRepository = securitySettingRepository;
_identityOptions = identityOptions.Value;
}
public LockoutOption GetSecuritySetting()
{
return _securitySettingRepository.GetSecuritySetting();
}
public LockoutOption UpdateSecuritySetting(LockoutOption lockoutOption)
{
var option = _securitySettingRepository.UpdateSecuritySetting(lockoutOption);
//update identity options
_identityOptions.Lockout.MaxFailedAccessAttempts = option.MaxFailedAccessAttempts;
return option;
}
}
Register in Startup
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.AddTransient<DbConnection>(serviceProvider => new DbConnection(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
// your rest configure services
services.AddTransient<ISecuritySettingService, SecuritySettingService>();
services.AddTransient<ISecuritySettingRepository, SecuritySettingRepository>();
var _ecuritySettingService = services.BuildServiceProvider().GetRequiredService<ISecuritySettingService>();
services.Configure<IdentityOptions>(options =>
{
options.Lockout.MaxFailedAccessAttempts = _ecuritySettingService.GetSecuritySetting()?.MaxFailedAccessAttempts ?? 3;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//your configure
}
}
Useage
namespace DapperPro.Controllers
{
public class LockoutOptionsController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IdentityOptions _identityOptions;
private readonly ISecuritySettingService _securitySettingService;
public LockoutOptionsController(ApplicationDbContext context
, IOptions<IdentityOptions> identityOptions
, ISecuritySettingService securitySettingService)
{
_context = context;
_identityOptions = identityOptions.Value;
_securitySettingService = securitySettingService;
}
// POST: LockoutOptions/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,AllowedForNewUsers,MaxFailedAccessAttempts,DefaultLockoutTimeSpan")] LockoutOption lockoutOption)
{
_securitySettingService.UpdateSecuritySetting(lockoutOption);
return View(lockoutOption);
}
}
}

Related

How to customise configuration binding in ASP.NET Core

I have a multi-tenant ASP.NET Core web application. The current tenancy model is every tenant has a separate web app and SQL database. I'm trying to rearchitect it so that multiple tenants will be served by a single web app (but maintaining a separate database per tenant). I've been following this series of blog posts but I've hit a bit of a roadblock with configuration.
The app makes heavy use of the ASP.NET Core configuration system, and has a custom EF Core provider that fetches config values from the database. I'd like to preserve this if possible, it would be an awful lot of work to rip out and replace with something else (dozens of config settings used in hundreds of places).
The existing code is very standard:
public class MyAppSettings
{
public string FavouriteColour { get; set; }
public int LuckyNumber { get; set; }
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<MyAppSettings>(Configuration.GetSection("MyAppSettings"));
// etc....
}
}
// custom EF Core config provider wired up in Program.Main, but that doesn't actually seem relevant
I've already updated our custom provider so that it fetches all configuration values from all known tenant databases, and adds them all to the configuration system, prefixed with a tenant identifier, so the list of all config values fetched from the n different databases might look something like this:
Key Value
===============================================
TenantABC:MyAppSettings:FavouriteColour Green
TenantABC:MyAppSettings:LuckyNumber 42
TenantDEF:MyAppsettings:FavouriteColour Blue
TenantDEF:MyAppSettings:LuckyNumber 37
...
TenantXYZ:MyAppSettings:FavouriteColour Yellow
TenantXYZ:MyAppSettings:LuckyNumber 88
What I'd like to be able to do is somehow customise the way that the configuration is bound so that it resolves the tenant for the current request, and then uses the appropriate values, e.g. a request on abc.myapp.com would observe config values "Green" and "42", etc, without having to change all the dependent places that inject IOptionsMonitor<AppSettings> (or IOptionsSnapshot, etc). The linked blog series has a post about configuration that covers some gotchas that I expect I'll eventually run into around caching etc, but it doesn't seem to cater for this scenario of using completely different settings for different tenants. Conceptually it seems simple enough, but I haven't been able to find the correct place to hook in. Please help!
Here is an idea (not tested yet, however). You can save the default IConfiguration instance passed to the constructor of your Startup class and then register in DI your own implementation of IConfiguration that will use that default one and HttpContextAccessor (to get the current tenant).
So the code will look something like:
public class Startup
{
private IConfiguration _defaultConfig;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
_defaultConfig = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
. . . .
services.AddScoped<IConfiguration>(serviceProvider => {
var httpContextAccessor =
serviceProvider.GetService<IHttpContextAccessor>();
return new MyConfig(_defaultConfig, httpContextAccessor);
});
}
. . . .
}
public class MyConfig : IConfiguration
{
private readonly IConfiguration _defaultConfig;
private readonly IHttpContextAccessor _httpContextAccessor;
public MyConfig(IConfiguration defaultConfig, IHttpContextAccessor httpContextAccessor)
{
_defaultConfig = defaultConfig;
_httpContextAccessor = httpContextAccessor;
}
public string this[string key] {
get {
var tenantId = GetTenantId();
return _defaultConfig[tenantId + ":" + key];
}
set {
var tenantId = GetTenantId();
_defaultConfig[tenantId + ":" + key] = value;
}
}
protected virtual string GetTenantId()
{
//this is just an example that supposes that you have "TenantId" claim associated with each user
return _httpContextAccessor.HttpContext.User.FindFirst("TenantId").Value; ;
}
public IEnumerable<IConfigurationSection> GetChildren()
{
return _defaultConfig.GetChildren();
}
public IChangeToken GetReloadToken()
{
return _defaultConfig.GetReloadToken();
}
public IConfigurationSection GetSection(string key)
{
var tenantId = GetTenantId();
return _defaultConfig.GetSection(tenantId + ":" + key);
}
}
Here are 3 solutions that may be helpful. I don't recommend you the IOptionsMonitor<T> because the tenant value is extracted from HttpContext, makes no sense to use the IOptionsMonitor.
Shared code:
public static class Extensions
{
public static string GetTenantName(this HttpContext context)
{
switch (context.Request.Host.Host)
{
case "abc.localhost.com":
return "TenantABC";
case "def.localhost.com":
return "TenantDEF";
default:
throw new IndexOutOfRangeException("Invalid host requested");
}
}
public static MyAppSettings GetAppSettingsByTenant(this IConfiguration config, string tenant)
{
return new MyAppSettings
{
LuckyNumber = int.Parse(config[$"{tenant}:MyAppSettings:LuckyNumber"]),
FavouriteColour = config[$"{tenant}:MyAppSettings:FavouriteColour"]
};
}
}
Solution 1: Scoped MyAppSettings object.
Registration (Startup->ConfigureServices(IServiceCollection)`
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped(sp =>
{
var contextAccessor = sp.GetService<IHttpContextAccessor>();
var config = sp.GetService<IConfiguration>();
return config.GetAppSettingsByTenant(contextAccessor.HttpContext.GetTenantName());
});
...
Usage:
public class TestController : Controller
{
private readonly MyAppSettings _settings;
public TestController(MyAppSettings settings)
{
_settings = settings;
}
[HttpGet]
public IActionResult Index()
{
return Json(_settings);
}
}
Solution 2: IOptions<MyAppSettings
Registration (Startup->ConfigureServices(IServiceCollection)`
public class MyAppSettingsOptions : IOptions<MyAppSettings>
{
public MyAppSettingsOptions(IConfiguration configuration, IHttpContextAccessor contextAccessor)
{
var tenant = contextAccessor.HttpContext.GetTenantName();
Value = configuration.GetAppSettingsByTenant(tenant);
}
public MyAppSettings Value { get; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOptions<MyAppSettings>, MyAppSettingsOptions>();
...
Usage
public class TestController : Controller
{
private readonly IOptions<MyAppSettings> _options;
public TestController(IOptions<MyAppSettings> options)
{
_options = options;
}
[HttpGet]
public IActionResult Index()
{
return Json(_options.Value);
}
}
Solution 3: IOptionsMonitor<MyAppSettings
Registration (Startup->ConfigureServices(IServiceCollection)`
public class MyAppSettingsOptionsMonitor : IOptionsMonitor<MyAppSettings>
{
public MyAppSettingsOptionsMonitor(IConfiguration configuration, IHttpContextAccessor contextAccessor)
{
var tenant = contextAccessor.HttpContext.GetTenantName();
CurrentValue = configuration.GetAppSettingsByTenant(tenant);
}
public MyAppSettings Get(string name)
{
throw new NotSupportedException();
}
public IDisposable OnChange(Action<MyAppSettings, string> listener)
{
return null;
}
public MyAppSettings CurrentValue { get; }
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IOptionsMonitor<MyAppSettings>, MyAppSettingsOptionsMonitor>();
...
Usage
public class TestController : Controller
{
private readonly IOptionsMonitor<MyAppSettings> _options;
public TestController(IOptionsMonitor<MyAppSettings> options)
{
_options = options;
}
[HttpGet]
public IActionResult Index()
{
return Json(_options.CurrentValue);
}
}
You can use DI services to config options
Sample code of your option class
public class MyAppSettings
{
public string FavouriteColor { get; set; }
public int LuckNumber { get; set; }
}
public interface IMySettingServices
{
string GetFavouriteColor();
int GetLuckNumber();
}
public class MySettingServices : IMySettingServices
{
private IHttpContextAccessor httpContextAccessor;
public MySettingServices(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetFavouriteColor()
{
var headers = this.httpContextAccessor.HttpContext.Request.Headers;
//Write your logic with httpContextAccessor by extract tenant here, then return actual config by tenant name
if(this.httpContextAccessor.HttpContext.Request.Host.Host=="abc.test.com")
{
//Get color setting for abc.test.com
}
return "Green";
}
public int GetLuckNumber()
{
var headers = this.httpContextAccessor.HttpContext.Request.Headers;
//Write your logic with httpContextAccessor by extract tenant here, then return actual config by tenant name
if (this.httpContextAccessor.HttpContext.Request.Host.Host == "abc.test.com")
{
//Get luck number setting for abc.test.com
}
return 1;
}
}
Sample code of your ConfigureService
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddControllersWithViews();
services.AddSingleton<IMySettingServices, MySettingServices>();
services.AddOptions<MyAppSettings>().Configure<IMySettingServices>((setting, settingServices) => {
setting.FavouriteColor = settingServices.GetFavouriteColor();
setting.LuckNumber = settingServices.GetLuckNumber();
});//This is the key point of this answer, you are delegating your setting assignment to a services, so you can do whatever you want in your services, in your word, customise configuration binding
}
Sample code of use your configuration in controller
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyAppSettings> settings;
public HomeController(ILogger<HomeController> logger, IOptions<MyAppSettings> settings)
{
_logger = logger;
this.settings = settings;
}
public IActionResult Index()
{
var favColor = settings.Value.FavouriteColor;
return View();
}
}
please be aware that when you want to access httpcontext, do not directly add services.AddScoped/AddSingleton/AddTransit<IHttpContextAccessor,HttpContextAccessor>(), this will result to DI system unable to resolve IHttpContextAccessor during ConfigureServices phases. Use services.AddHttpContextAccessor(); is the best way to do that

AllowAnonymous is not ignored in ASP.NET Core custom AuthenticationHandler after migrating from .NET Core 2.2 to 3.1

I have an ASP.NET Core 2.2 Web API which was working with the Basic Authentication. So far it worked fine and has no troubles. In one of the Controller, one Action Method is Decorated with [AllowAnonymous] to make the User Login, as usual.
[Produces("application/json")]
[Route("user")]
[AllowAnonymous]
[ApiController]
public class LoginController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IMessagingService _messageService;
private readonly IBasicAuthenticationService _basicAuthenticationService;
private readonly string PWAPIBaseUrl;
public LoginController(IConfiguration configuration, ILogger<LoginController> logger, IMessagingService messagingService, IBasicAuthenticationService authenticationService)
{
_configuration = configuration;
_logger = logger;
_messageService = messagingService;
_basicAuthenticationService = authenticationService;
}
[HttpGet]
[AllowAnonymous]
[Route("login/{username}/{clientID}")]
public async Task<IActionResult> UserLogin(string username, string clientID)
{
// Check the Credentials Manually
string failReason = "";
if (!CheckCredentials(out failReason))
{
return StatusCode(StatusCodes.Status403Forbidden, userInfo);
}
// Load the Roles and UI Preferences ...
}
}
As the end of .NET Core 2.2 is near, I have tried upgrading to the .NET Core 3.1 and followed the official Migration Guide and made necessary changes. Though the Application started out smoothly, there is one bugging issue which forbids the upgrade.
On the above controller, the [AllowAnonymous] is not ignored and the Authentication is evaluated and thrown out with an error. But the Login method is executed after. This causes Login to break in all the dependent applications. I have tried all the suggestions from Stackoverflow like this, this and this.
Basic Authentication Handler:
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly ILogger<BasicAuthenticationHandler> _logger = null;
private readonly IBasicAuthenticationService _basicAuthenticationService;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
UrlEncoder encoder,
ILoggerFactory loggerFactory,
ISystemClock clock,
IBasicAuthenticationService authenticationService)
: base(options, loggerFactory, encoder, clock)
{
_logger = loggerFactory.CreateLogger<BasicAuthenticationHandler>();
_basicAuthenticationService = authenticationService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var config = Util.GetConfig();
if (!Request.Headers.ContainsKey("Authorization"))
{
_logger.LogError("No authorization credentials");
return AuthenticateResult.NoResult();
}
if (!Request.Headers.ContainsKey("ClientID"))
{
_logger.LogError("Missing header client token");
return AuthenticateResult.Fail("Missing header client token");
}
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
if (authHeader.Scheme != "Basic")
{
_logger.LogError("Authentication scheme not recognized");
return AuthenticateResult.Fail("Authentication scheme not recognized");
}
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
var username = credentials[0];
var password = credentials[1];
string fullname = "";
string failReason = "";
bool t = false;
IPrincipal principal = null;
// Do Business Validation against the DB
if (!t) // login failed
{
byte[] bEncodedResponse = Encoding.UTF8.GetBytes(failReason);
await Context.Response.Body.WriteAsync(bEncodedResponse, 0, bEncodedResponse.Length);
return AuthenticateResult.Fail(failReason);
}
else
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, username),
new Claim(ClaimTypes.Name, fullname),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
principal = principal==null?new ClaimsPrincipal(identity): principal;
var ticket = new AuthenticationTicket(principal as ClaimsPrincipal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
Startup.cs
public class Startup
{
public Startup(IWebHostEnvironment environment, IConfiguration configuration, ILoggerFactory loggerFactory)
{
Environment = environment;
Configuration = configuration;
LoggerFactory = loggerFactory;
}
public IConfiguration Configuration { get; }
public ILoggerFactory LoggerFactory { get; }
public IWebHostEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
// Adding the Configuration Options -- Extension Methods to Inject Configuration as IOption POCOs
services.ConfigureAPIOptions(Configuration);
// configure DI for application services -- Other DI Objects
services.ConfigureDependencies(Configuration, LoggerFactory);
Common.APIConfiguration.Current = Configuration;
services.AddControllers();
services.AddAuthorization();
if (Environment.IsDevelopment())
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "My Materials API", Version = "v1" });
});
}
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My Materials API v1");
c.RoutePrefix = string.Empty;
});
}
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I am still clueless on what I did wrong and I might be missing something in ASP.NET Core 3.1. Please help me in getting this working. Thanks in advance.
EDIT 1:
ServiceExtensions.cs
public static class ServiceExtensions
{
public static void ConfigureAPIOptions(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions();
services.Configure<DataSetting>(configuration.GetSection("DataSettings"));
services.Configure<UrlSetting>(configuration.GetSection("UrlSettings"));
services.Configure<SiteSettings>(configuration.GetSection("SiteSettings"));
}
public static void ConfigureDependencies(this IServiceCollection services, IConfiguration configuration, ILoggerFactory loggerFactory)
{
services.AddSingleton<IConfiguration>(configuration);
services.AddScoped<IBasicAuthenticationService, BasicAuthenticationService>();
services.AddScoped<IMessagingService>(s => new MessagingServices(configuration, loggerFactory.CreateLogger<MessagingServices>()));
services.AddHostedService<TimedHostedService>();
}
}
A Small Kludge for accessing the Configuration, where DI is not possible.
public static class APIConfiguration
{
public static IConfiguration Current { get; set; }
}
I tried this and it really helps me.
private static bool HasAllowAnonymous(AuthorizationFilterContext context)
{
var filters = context.Filters;
for (var i = 0; i < filters.Count; i++)
{
if (filters[i] is IAllowAnonymousFilter)
{
return true;
}
}
// When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that
// were discovered on controllers and actions. To maintain compat with 2.x,
// we'll check for the presence of IAllowAnonymous in endpoint metadata.
var endpoint = context.HttpContext.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
return true;
}
return false;
}
https://github.com/dotnet/aspnetcore/blob/bd65275148abc9b07a3b59797a88d485341152bf/src/Mvc/Mvc.Core/src/Authorization/AuthorizeFilter.cs#L236
It was mentioned here https://learn.microsoft.com/en-us/dotnet/core/compatibility/2.2-3.1#authorization-iallowanonymous-removed-from-authorizationfiltercontextfilters

How to use ITranscriptLogger and TranscriptLoggerMiddleware to store chat transcript in cosmos DB

I want to store the entire chat history in cosmos DB using ITranscriptLogger and TranscriptMiddelWare, but I am struggling to do so. I have read this MS article, but I want to store it in Cosmos DB and not Blob storage. Also I am trying to instantiate the transcript log in Startup.cs not in Bot.cs, and I have tried to implement it according to this answer without any luck. That is, the transcript is not stored and there's no container in my Azure cosmos DB. I appreciate any help and feedback.
Code:
I have created the TranscriptStore class and created and added the middleware as instructed in the referenced SO answer:
CosmosTranscriptStore.cs
public class CosmosTranscriptStore : ITranscriptLogger
{
private CosmosDbStorage _storage;
public CosmosTranscriptStore(CosmosDbStorageOptions config)
{
_storage = new CosmosDbStorage(config);
}
public async Task LogActivityAsync(IActivity activity)
{
// activity only contains Text if this is a message
var isMessage = activity.AsMessageActivity() != null ? true : false;
if (isMessage)
{
// Customize this to save whatever data you want
var data = new
{
From = activity.From,
To = activity.Recipient,
Text = activity.AsMessageActivity().Text,
};
var document = new Dictionary<string, object>();
// activity.Id is being used as the Cosmos Document Id
document.Add(activity.Id, data);
await _storage.WriteAsync(document, new CancellationToken());
}
}
}
Startup.cs
public class Startup
{
private const string CosmosServiceEndpoint = "MyCosmosServiceEndpoint";
private const string CosmosDBKey = "MyCosmosDBKey";
private const string CosmosDBDatabaseName = "MyCosmosDBDatabaseName";
private const string CosmosDBCollectionName = "Transcript-storage";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var config = new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBCollectionName,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
};
var transcriptMiddleware = new TranscriptLoggerMiddleware(new CosmosTranscriptStore(config));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Create the Bot Framework Adapter.
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddSingleton<MainDialog>();
services.AddTransient<IBot, WelcomeBot<MainDialog>>();
services.AddBot<WelcomeBot<MainDialog>>(options =>
{
var middleware = options.Middleware;
middleware.Add(transcriptMiddleware);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc();
}
}
}
I managed to solve this by adding the transcript store middleware to the adapter, which I probably should have done from the beginning before asking this question, but I am very new to bot framework and this type of programming all together.
This is how I solved it:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
var config = new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBAntoherCollectionName,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
};
var transcriptMiddleware = new TranscriptLoggerMiddleware(new CosmosTranscriptStore(config));
services.AddSingleton(transcriptMiddleware);
...
}
AdapterWithErrorHandler.cs
public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
{
public AdapterWithErrorHandler(TranscriptLoggerMiddleware transcriptMiddlewareStore, IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger, ConversationState conversationState = null)
: base(configuration, logger)
{
Use(transcriptMiddlewareStore);
OnTurnError = async (turnContext, exception) =>
{
...
};
}
}
In addition, if one wants to store the entire chat transcript in one document/item I would highly recommend storing the data in the CosmosTranscriptStore class by conversation ID instead of activity ID. The reason is that every activity has its own ID thus creating a new item in Cosmos DB for every activity.
public class CosmosTranscriptStore : ITranscriptLogger
{
...
public async Task LogActivityAsync(IActivity activity)
{
...
document.Add(activity.Conversation.Id, data);
await chatStorage.WriteAsync(document, new CancellationToken());
}
}

Access the current HttpContext in ASP.NET Core with Custom

I am writing code for following: Access the current HttpContext in ASP.NET Core
I am receiving error. How would I resolve this?
Also, whats the code for Interface IMyComponent? Just want to be sure its correct.
Errors:
Type or namespace IMyComponent Cannot be found
The Name 'KEY' does not exist in current context.
public class MyComponent : IMyComponent
{
private readonly IHttpContextAccessor _contextAccessor;
public MyComponent(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public string GetDataFromSession()
{
return _contextAccessor.HttpContext.Session.GetString(*KEY*);
}
}
Some points you need to pay attention to:
1.You class inherit from an interface and implement a GetDataFromSession method.You need to define an interface IMyComponent first and register IMyComponent in staryup if you would like use by DI
public interface IMyComponent
{
string GetDataFromSession();
}
startup.cs
services.AddSingleton<IMyComponent, MyComponent>();
2.It seems that you would like to get data from session. The "Key" represents any session name (string).You need to enable session for asp.net core and set a session value first.
_contextAccessor.HttpContext.Session.SetString("Key", "value");
3.Register IHttpContextAccessor in your startup
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
4.Full demo:
MyComponent.cs
public class MyComponent : IMyComponent
{
private readonly IHttpContextAccessor _contextAccessor;
public MyComponent(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public string GetDataFromSession()
{
_contextAccessor.HttpContext.Session.SetString("Key", "value");
return _contextAccessor.HttpContext.Session.GetString("Key");
}
}
public interface IMyComponent
{
string GetDataFromSession();
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IMyComponent, MyComponent>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//other middlewares
app.UseSession();
app.UseMvc();
}
}
API Controller:
public class ForumsController : ControllerBase
{
private readonly IMyComponent _myComponent;
public ForumsController(IMyComponent myComponent)
{
_myComponent = myComponent;
}
// GET api/forums
[HttpGet]
public ActionResult<string> Get()
{
var data = _myComponent.GetDataFromSession();//call method and return "value"
return data;
}

Access HttpContextAccessor from Helper Class in .net core web api

I want to access JwtHelper from ExceptionHelper. But problem is ExceptionHelper must be static. And so, we can't create constructor and not access jwtHelper Method. How can I achieve access jwHelper from ExcewptionHelper.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddMvc();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDbContext<MyDbContext>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
context.Response.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
ExceptionHelper.cs
public static class ExceptionHelper
{
public static async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = jwtHelper.GetValueFromToken(token, "UserId");??????
//in this line I can't access jwtHelper.
}
}
JwtHelper.cs
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string stream, string propertyName)
{
var jwt = httpContextAccessor.HttpContext.Request.Headers["Authorization"];
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(stream.Replace("Bearer ", "")) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
If I were you I would register JwtHelper with a Interface known as IJwtHelper.
It would look like this then
public class JwtHelper : IJwtHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public JwtHelper(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetValueFromToken(string propertyName)
{
var jwt= httpContextAccessor.HttpContext.Request.Headers["Authorization"];
// I can't access httpContextAccessor in this line.
var handler = new JwtSecurityTokenHandler();
var tokens = handler.ReadToken(jwt) as JwtSecurityToken;
return tokens.Claims.FirstOrDefault(claim => claim.Type == propertyName).Value;
}
}
public interface IJwtHelper
{
string GetValueFromToken(string propertyName);
}
In my startup.cs class I would then do
services.AddSingleton<IJwtHelper, JwtHelper>();
And then when you want to access your helper I would inject IJwtHelper
private IJwtHelper _jwtHelper;
public SomeConstructerOnClass(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public void SomeMethod(string property) {
var token = _jwtHelper.GetValueFromToken(property);
//Do something with token
}
where _jwtHelper is field of type IJwtHelper.
You will then be able to use GetValueFromToken quite fine anywhere you inject IJwtHelper
UPDATE
Your problem is that ExceptionHandler is Static , implement an interface and add it to container
public class ExceptionHelper : IExceptionHelper
{
private IJwtHelper _jwtHelper;
public ExceptionHelper(IJwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
public async Task AddApplicationError(this HttpResponse response)
{
Log log = new Log();
log.UserId = _jwtHelper.GetValueFromToken(token, "UserId");??????
}
}
public interface IExceptionHelper
{
Task AddApplicationError( HttpResponse response);
}
Then
services.AddSingleton<IExceptionHelper, ExceptionHelper>();
Now You will be able to inject it into your Configure method like so
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IExceptionHelper exceptionHelper)
{
app.UseExceptionHandler(builder => builder.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>();
//Resolved and available!
exceptionHelper.AddApplicationError(error);
await context.Response.WriteAsync(error.Error.Message);
}));
app.UseHttpsRedirection();
app.UseMvc();
}
If you follow me advice above from my initial response and my update everything should be fine and registered nicely in your container :)
You'll have to instantiate the JwtHelper class in order to access the instance variable (httpContextAccessor) from another class. Static methods, like GetValueFromToken, cannot access instance variables.

Categories