Hangfire aspnetcore2 default authentication challenge not working - asp.net-core

Using hangfire version: 1.6.17
I have successfully setup hangifire on aspnetcore 2.0
I added authorization by using:
app.UseHangfireDashboard("/jobs", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() }
});
and
public class HangfireAuthorizationFilter :IDashboardAuthorizationFilter
{
private const string PERMISSION = "read:jobs";
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// allow only users with correct permission
if (httpContext.User.Identity.IsAuthenticated)
{
var permissions = httpContext.User.Claims.FirstOrDefault(x => x.Type.Equals(CustomClaims.Permissions))?.Value?.Split(' ');
return permissions?.Contains(PERMISSION) ?? false;
}
return false;
}
}
The only problem i cannot resolve is that a blank screen with 401 is returned to the user instead of the default challenge /account/login.
If you access my controllers with the [Authorize] attribute, they are automatically redirected to /account/login, so the loginpath is working.
Even if i specify it specifically, the user is not redirected while accessing Hangfire unauthorised:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login/";
})
Somebody an idea or should i mark it as a bug at Hangfire github.

First you should add HangFireAuthorizationFilter
public sealed class HangFireAuthorizationFilter : IDashboardAuthorizationFilter
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
public HangFireAuthorizationFilter(IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
}
public bool Authorize([NotNull] DashboardContext context)
{
var httpContext = context.GetHttpContext();
return httpContext.User.Identity.IsAuthenticated;
}
}
Then use the below in the startup:
var hangFireAuth = new DashboardOptions
{
Authorization = new[]
{
new HangFireAuthorizationFilter(app.ApplicationServices.GetService<IAuthorizationService>(),
app.ApplicationServices.GetService<IHttpContextAccessor>())
},
AppPath = "/login"
};
app.UseHangfireDashboard("/hangfire", options: hangFireAuth);
You can refer to the following https://medium.com/ricos-note/hangfire-dashboard-of-authorization-b41d7135b044 for more details

Related

Why is my custom `AuthentictionStateProvider` not null in AddSingleton but null in AddScoped

I had previously asked a question that was answered properly, but the problem is that when my custom AuthenticationStateProvider is registered as a scoped
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
I get the following error:
System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState
But, when it is registered as a singleton, it works correctly, However, the single instance creates for the lifetime of the application domain by AddSingelton, and so this is not good.(Why? Because of :))
What should I do to register my custom AuthenticationStateProvider as a scoped, but its value is not null?
Edit:
According to #MrC aka Shaun Curtis Comment:
It's my CustomAuthenticationStateProvider:
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
: base(loggerFactory) =>
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
And it's a program configuration and service registration:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
#region Authentication
//Authentication
services.AddDbContextFactory<ApplicationDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("LocalDBConnection"),
serverDbContextOptionsBuilder =>
{
var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
serverDbContextOptionsBuilder.CommandTimeout(minutes);
serverDbContextOptionsBuilder.EnableRetryOnFailure();
})
.AddInterceptors(new CorrectCommandInterceptor()); ;
});
//add policy
services.AddAuthorization(options =>
{
options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
});
// Needed for cookie auth.
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/";
options.LogoutPath = "/login";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
#endregion
//AutoMapper
services.AddAutoMapper(typeof(MappingProfile).Assembly);
//CustomAuthenticationStateProvider
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
.
.
}
Don't worry about the AddSingelton in the Blazor apps. Scoped dependencies act the same as Singleton registered dependencies in Blazor apps (^).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
The Blazor Server hosting model supports the Scoped lifetime across HTTP requests (Just for the Razor Pages or MVC portion of the app) but not across SignalR connection/circuit messages among components that are loaded on the client.
That's why there's a scope.ServiceProvider.GetRequiredService here to ensure the retrived user is fetched from a new scope and has a fresh data.
Actually this solution is taken from the Microsoft's sample.
Your problem is probably here:
var scope = _scopeFactory.CreateScope();
/...
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
You create a new IOC container and request the instance of IUsersService from that container.
If IUsersService is Scoped, it creates a new instance.
IUsersService requires various other services which the new container must provide.
public UsersService(IUnitOfWork uow, ISecurityService securityService, ApplicationDbContext dbContext, IMapper mapper)
Here's the definition of those services in Startup:
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>();
services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>();
IUnitOfWork and ISecurityService are both Scoped, so it creates new instances of these in the the new Container. You almost certainly don't want that: you want to use the ones from the Hub SPA session container.
You have a bit of a tangled web so without a full view of everything I can't be sure how to restructure things to make it work.
One thing you can try is to just get a standalone instance of IUsersService from the IOC container using ActivatorUtilities. This instance gets instantiated with all the Scoped services from the main container. Make sure you Dispose it if it implements IDisposable.
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceProvider _serviceProvider;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
: base(loggerFactory) =>
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get an instance of IUsersService from the IOC Container Service to ensure it fetches fresh data
IUsersService userManager = null;
try
{
userManager = ActivatorUtilities.CreateInstance<IUsersService>(_serviceProvider);
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
userManager?.Dispose();
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
For reference this is my test code using the standard ServerAuthenticationStateProvider in a Blazor Server Windows Auth project.
public class MyAuthenticationProvider : ServerAuthenticationStateProvider
{
IServiceProvider _serviceProvider;
public MyAuthenticationProvider(IServiceProvider serviceProvider, MyService myService)
{
_serviceProvider = serviceProvider;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
MyService? service = null;
try
{
service = ActivatorUtilities.CreateInstance<MyService>(_serviceProvider);
// Do something with service
}
finally
{
service?.Dispose();
}
return base.GetAuthenticationStateAsync();
}
}

Identity Server 4 Authorization Flow

Identity server 4 and ASP.Net Core 3 noob here, what I'm trying to do is kinda simple: a client should do an HTTP Get request to a remote controller and retrieve some data after being authorized by the Identity Server.
I'm using a code flow as GrantType.
Identity Server services configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// Identity server database
string connectionString = Config.GetConnectionString("IdentityServerDatabase");
services.AddDbContext<IdentityServerDatabaseContext>(config =>
{
config.UseSqlServer(connectionString);
});
// Easy password for testing
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.Password.RequiredLength = 4;
config.Password.RequireDigit = false;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequireUppercase = false;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<IdentityServerDatabaseContext>()
.AddDefaultTokenProviders();
services.AddMvc();
services.AddIdentityServer()
// Identity User extension
.AddAspNetIdentity<ApplicationUser>()
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
services.ConfigureApplicationCookie(config =>
{
config.Cookie.Name = "Server.Cookie";
config.LoginPath = "/Home/Login";
config.LogoutPath = "/Home/Logout";
});
services.Configure<DataProtectionTokenProviderOptions>(opt =>
opt.TokenLifespan = TimeSpan.FromHours(12));
}
//This is the IdentityServer jwt bearer, I'm using this because my IdentityServer is also an API that needs authorization, which I use for retrieving my users data:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", config =>
{
// Identity Server authority
config.Authority = "https://localhost:44300/";
config.Audience = "IdentityAPI";
});
This is the Identity Server configuration for accepted clients:
public class Configuration
{
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope> {
new ApiScope("IdentityAPI"),
new ApiScope("ClientGateway"),
};
public static IEnumerable<ApiResource> GetApiResources() =>
new List<ApiResource> {
new ApiResource("IdentityAPI")
{
Scopes = { "IdentityAPI" }
},
new ApiResource("ClientGateway")
{
Scopes = { "ClientGateway" }
},
};
public static IEnumerable<Client> GetClients() =>
new List<Client> {
// Client for my IdentityServer
new Client {
ClientId = "IdentityAPI",
ClientSecrets = { new Secret("secret_IdentityAPI".ToSha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = {
"IdentityAPI",
},
};
// My Client configuration, this client is trying to retrieve users data from the IdentityServer
new Client {
ClientId = "ClientGateway",
ClientSecrets = { new Secret("secret_ClientGateway".ToSha256()) },
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
RequirePkce = true,
// AllowedGrantTypes = GrantTypes.ClientCredentials,
// Necessary for authorization code flow type
// where to redirect to after login
RedirectUris = { "https://localhost:44400/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:44400/Home/Index" },
RequireConsent = false,
AllowedScopes = {
"IdentityAPI",
"ClientGateway",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
},
}
This is the client Startup:
public class Startup
{
private readonly IConfiguration Config;
private readonly IWebHostEnvironment Env;
public Startup(IConfiguration config, IWebHostEnvironment env)
{
Config = config;
Env = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(config =>
{
config.DefaultScheme = "Cookie";
config.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookie")
.AddOpenIdConnect("oidc", config =>
{
config.SignInScheme = "Cookie";
config.ClientId = "ClientGateway";
config.ClientSecret = "secret_ClientGateway";
config.SaveTokens = true;
config.Authority = "https://localhost:44300/";
config.ResponseType = "code";
config.SignedOutCallbackPath = "/Home/Index";
});
services.AddHttpClient();
services.AddControllersWithViews();
}
}
This is the Client Controller that is trying to authenticate and retrieve the data:
[Route("api/[controller]")]
[ApiController]
public class IdentityController : ControllerBase
{
private static readonly string IdentityAPIUrl = "https://localhost:44300";
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
public IdentityController(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
{
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
}
public static async Task<HttpClient> NewClientGatewayHttpClient(IHttpClientFactory _httpClientFactory)
{
var serverClient = _httpClientFactory.CreateClient();
var discoveryDocument = await serverClient.GetDiscoveryDocumentAsync(IdentityAPIUrl);
var tokenResponse = await serverClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = discoveryDocument.TokenEndpoint,
ClientId = "ClientGateway",
ClientSecret = "secret_ClientGateway",
Scope = "IdentityAPI",
});
var apiClient = _httpClientFactory.CreateClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
return apiClient;
}
// I'm calling this function to retrieve users data from my IdentityServer
[HttpGet]
[Route("IDUsers")]
public async Task<List<ApplicationUser>> GetIDUsers()
{
HttpClient apiClient = ModisIDController.NewClientGatewayHttpClient(_httpClientFactory).Result;
// This response return a StatusCode: 200
var response = await apiClient.GetAsync(IdentityAPIUrl + "/api/get/users");
// In jsonResult I'm expecting my users data, but instead I'm receiving my view Login.cshtml as a string
var jsonResult = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<ApplicationUser>>(jsonResult);
}
This is the IdentityServer function that the Client Controller is trying to call:
[Authorize]
[Route("api/[controller]")]
[ApiController]
// Use this controller for Read API functions
public class GetController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IdentityServerDatabaseContext _context;
public GetController(UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IdentityServerDatabaseContext context)
{
_userManager = userManager;
_roleManager = roleManager;
_context = context;
}
[HttpGet]
[Route("Users")]
public IActionResult GetUsers()
{
return Ok(_context.Users.ToList());
}
}
When ApiController tries to retrieve the data it gets correctly redirected to my Login view by the Identity Server, after login the user I get this output from my IdentityServer:
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryKeyEndpoint for /.well-known/openid-configuration/jwks
info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
info: IdentityServer4.Validation.TokenRequestValidator[0]
Token request validation success, {
"ClientId": "ClientGateway",
"GrantType": "client_credentials",
"Scopes": "IdentityAPI",
"Raw": {
"grant_type": "client_credentials",
"scope": "IdentityAPI",
"client_id": "ClientGateway",
"client_secret": "***REDACTED***"
}
}
But the response that I retrieve in var content is HTML of the Login page and not the expected data. It seems like that the Identity Server is trying to redirect me again. I don't undestard why this happens because I'm using client credentials flow.

OAuth .NET Core - to many redirects after sign in with custom options - Multi Tenant

I'm trying to implement OAuth per tenant. Each tenant has their own OAuthOptions.
I overwritten OAuthOptions and with IOptionsMonitor i resolve the OAuthOptions every time. Based on this answer: openid connect - identifying tenant during login
But right now after log in on external, the callback to signin ends up in to many redirects.
Signin successfull -> redirect to app -> app says not authenticated -> redirect to external login -> repeat.
The code:
My Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddAuthentication(options =>
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "OAuth";
})
.AddCookie("OAuth.Cookie", options =>
{
options.Cookie.Name = "OAuth-cookiename";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/login";
})
.AddOAuth<MyOptions, OAuthHandler<MyOptions>>("OAuth", options =>
{
// All options are set at runtime by tenant settings
});
services.AddScoped<IOptionsMonitor<MyOptions>, MyOptionsMonitor>();
services.AddScoped<IConfigureOptions<MyOptions>, ConfigureMyOptions>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Startup.Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment 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.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
ConfigureMyOptions.cs
public class ConfigureMyOptions : IConfigureNamedOptions<MyOptions>
{
private HttpContext _httpContext;
private IDataProtectionProvider _dataProtectionProvider;
private MyOptions myCurrentOptions;
public ConfigureMyOptions(IHttpContextAccessor contextAccessor, IDataProtectionProvider dataProtectionProvider)
{
_httpContext = contextAccessor.HttpContext;
_dataProtectionProvider = dataProtectionProvider;
}
public void Configure(string name, MyOptions options)
{
//var tenant = _httpContext.ResolveTenant();
// in my code i use tenant.Settings for these:
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.ClientId = "redacted";
options.ClientSecret = "redacted";
options.Scope.Add("openid");
options.Scope.Add("write:gpg_key");
options.Scope.Add("repo");
options.Scope.Add("read:user");
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey("External", "id");
options.SignInScheme = "OAuth.Cookie";
options.CallbackPath = new PathString("/signin");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = _onCreatingTicket,
OnTicketReceived = _onTicketReceived
};
myCurrentOptions = options;
}
public void Configure(MyOptions options) => Configure(Options.DefaultName, options);
private static async Task _onCreatingTicket(OAuthCreatingTicketContext context)
{
// Get the external user id and set it as a claim
using (var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint))
{
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
using (var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted))
{
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
}
}
private static Task _onTicketReceived(TicketReceivedContext context)
{
context.Properties.IsPersistent = true;
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30);
return Task.CompletedTask;
}
}
MyOptions:
// Overwritten to by pass validate
public class MyOptions : OAuthOptions
{
public override void Validate()
{
return;
}
public override void Validate(string scheme)
{
return;
}
}
MyOptionsMonitor:
// TODO caching
public class MyOptionsMonitor : IOptionsMonitor<MyOptions>
{
// private readonly TenantContext<Tenant> _tenantContext;
private readonly IOptionsFactory<MyOptions> _optionsFactory;
public MyOptionsMonitor(
// TenantContext<Tenant> tenantContext,
IOptionsFactory<MyOptions> optionsFactory)
{
// _tenantContext = tenantContext;
_optionsFactory = optionsFactory;
}
public MyOptions CurrentValue => Get(Options.DefaultName);
public MyOptions Get(string name)
{
return _optionsFactory.Create($"{name}");
}
public IDisposable OnChange(Action<MyOptions, string> listener)
{
return null;
}
}

Identity.IsAuthenticated returning false after SignInAsync() in HTTPGET controllers

This application should automatically sign-in users using their Environment.Username, but I'm struggling to do it.
In the following HomeController, the variable "ThisGivesNegative" remains false even after the "HttpContext.SignInAsync" is invoked.
When I put this code in an HTTPPost action, the sign in is correct so I guess there has to be something with the configuration but after navigating in the web none of the StackOverflow posts worked.
Could any of you give me a hand? Thanks!
public class HomeController : Controller
{
private readonly IAppUserService _appUserService;
private readonly ILogger _logger;
private readonly ApplicationDbContext _context;
public HomeController(
ApplicationDbContext context,
IAppUserService appUserService,
ILoggerFactory loggerFactory
)
{
_context = context;
_appUserService = appUserService;
_logger = loggerFactory.CreateLogger<AccountController>();
}
public async Task<IActionResult> Index()
{
string WindowsUsername = Environment.UserName;
if (WindowsUsername != null)
{
List<AppRole> RolesForThisUser = new List<AppRole>();
RolesForThisUser = _context.AppUserAppRoles.Where(x => x.AppUser.ApexID == WindowsUsername).Select(x => x.AppRole).ToList();
var properties = new AuthenticationProperties
{
//AllowRefresh = false,
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1)
};
List<Claim> MyClaims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, WindowsUsername),
};
foreach (AppRole ThisRole in RolesForThisUser)
{
MyClaims.Add(new Claim(ClaimTypes.Role, ThisRole.RoleName.ToString()));
}
var identity = new ClaimsIdentity(MyClaims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal, properties);
bool ThisGivesNegative = HttpContext.User.Identity.IsAuthenticated;
}
return View();
}
}
Here my ConfigureServices code:
public void ConfigureServices(IServiceCollection services)
{
services
.Configure<API_IP21_CurrentValues>(ConfigAppSettings.GetSection("API_IP21_CurrentValues"))
.Configure<API_IP21_HistoricValues>(ConfigAppSettings.GetSection("API_IP21_HistoricValues"))
.Configure<API_PPM_DailyValues>(ConfigAppSettings.GetSection("API_PPM_DailyValues"))
.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
})
.AddDbContextPool<ApplicationDbContext>(options =>
{
options.UseSqlServer(ConfigAppSettings.GetSection("ConnectionStrings").GetSection("DefaultConnection").Value);
})
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Views/Home/Index.cshtml";
options.LogoutPath = "/Views/Home/Index.cshtml";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddTransient<IAppUserService, AppUserService>()
.AddTransient<IEquipmentRepository, EquipmentRepository>()
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
I found the issue, here some help for those in need!
Esentially, cookies are read before the Home Controller is fired so when the view is rendered, the program doesn't know this user has those cookies. We need to reload.
I solve this by adding the following code just before "return view()" in the Home Controller.
if (FirstPostback == false)
{
RedirectToAction("Index", "Home", new { FirstPostback = true});
}
return View(new LoginViewModel { ReturnUrl = returnUrl });

Unable to resolve service for type at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider

I am attempting to receive data from the server controller, stocks.
I get this error:
"System.InvalidOperationException: Unable to resolve service for type myBackEnd.Models.StockContext' while attempting to activate 'myBackEnd.Controllers.StockController'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService
(IServiceProvider sp, Type type, Type requiredBy, Boolean
isDefaultParameterRequired"
Here is my stocks controller code:
namespace myBackEnd.Controllers
{
[Route("api/stock")]
[Produces("application/json")]
public class StockController : ControllerBase
{
private readonly int fastEmaPeriod = 10;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Models.StockContext _context;
public StockController(Models.StockContext context, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_context = context;
}
// POST api/values
[HttpPost]
public async Task<IActionResult> Post([FromBody]Models.Stock stock)
{
_context.Stocks.Add(stock);
await _context.SaveChangesAsync();
return Ok(stock);
}
This is the startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("MyPolicy", corsBuilder =>
{
corsBuilder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
services.AddDbContext<DataContext>(x => x.UseInMemoryDatabase("TestDb"));
services.AddHttpClient();
services.AddAutoMapper();
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// configure DI for application services
services.AddScoped<IUserService, UserService>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
This worked before I added the registration, login and
// configure DI for application services
services.AddScoped();
The problem was the DB context was not registered for dependency injection.
Adding:
services.AddDbContext<Models.StockContext>(opt => opt.UseInMemoryDatabase("item"));
fixed the problem.