Reputation: 820
I have a .NET6 Blazor wasm application that uses IdentityServer4 for authentication. I want to create a login event in database whenever a user logs in. To do that, I am using the IdentityServer ClientAuthenticationSuccess
event as follows. The issue is that user
is always null that I am getting from HttpContextAccessor
:
public class CustomEventSink : IEventSink
{
private readonly ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserManager<ApplicationUser> _userManager;
public CustomEventSink(ApplicationDbContext context, UserManager<ApplicationUser> user, IHttpContextAccessor contextAccessor)
{
_context = context;
_httpContextAccessor = contextAccessor;
_userManager = user;
}
public async Task PersistAsync(Event evt)
{
//log event here....
EventLog el = new();
if (@evt.Id.Equals(EventIds.ClientAuthenticationSuccess))
{
try
{
var user = await _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User);
if (user != null)
{
// do stuff
el.EventName = "Login";
el.EventDescription = $"{evt.Name} ({evt.Id}), Details:{evt}";
el.Created = DateTime.Now;
el.UserName = user.UserName;
_context.EventLog.Add(el);
_context.SaveChanges();
}
}
catch (Exception ex)
{
// handle exception
}
}
if (@evt.Id.Equals(EventIds.ClientAuthenticationFailure))
{
try
{
// do stuff
el.EventName = "Login Failure";
el.EventDescription = $"{evt.Name} ({evt.Id}), Details:{evt}";
el.Created = DateTime.Now;
_context.EventLog.Add(el);
_context.SaveChanges();
}
catch (Exception ex)
{
// handle exception
}
}
}
}
The problem is that user
is always null.
Startup.cs:
namespace P.Server
{
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.AddSyncfusionBlazor();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), new MySqlServerVersion(new Version(8, 0, 26))));
services.AddOptions();
services.AddControllers();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization();
services.AddHttpContextAccessor();
services.AddSession();
//services.AddSingleton<IUserIdProvider, NameUserIdProvider>(); //this is for signalr
services.AddControllersWithViews();
services.AddRazorPages();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
services.AddServerSideBlazor();
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.AddMvc().AddMvcOptions(options =>
{
options.EnableEndpointRouting = false;
});
services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMvc().AddXmlDataContractSerializerFormatters();
services.AddSignalR();
services.AddTransient<IEventSink, CustomEventSink>();
//services.AddCors(options =>
//{
// options.AddPolicy("EnableCORS", builder =>
// {
// builder.AllowAnyOrigin().AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials().Build();
// });
//});
//services.AddMvc(option => option.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
app.UseResponseCompression();
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.UseSession();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapHub<Broadcaster>("/broadcaster");
endpoints.MapHub<LoginCountHub>("/LoginCountHub");
endpoints.MapFallbackToFile("index.html");
});
CreateRoles(serviceProvider).Wait();
}
...
}
}
Upvotes: 1
Views: 292
Reputation: 2280
If you call the token endpoint from the browser you should be sending the session Cookie with the authentication information. This information is passed to the HttpContext in the authentication middleware. You have configured this middleware after IdentityServer4 middleware, but IdentityServer4 adds authentication middleware internally when you call to AddIdentityServer method and it's added just before the endpoints. Yes, you have the authentication middleware twice.
You could check if this code populates the principal before trying to acces HttpContext.User property:
await httpContext.AuthenticateAsync();
If this doesn't work maybe you aren't persisting the cookie for some reason. Can you check if the browser is sending the cookie in every call?
Anyway, the event ClientAuthenticationSuccess is raised when the Client is authenticated (clientId and clientSecret) in the token endpoint. That occurs in a different call from the user login.
If you want to log user logins I'd use better the UserLoginSuccess event. You could cast the event to UserLoginSuccessEvent and there you can access the SubjectId and DisplayName properties from the user. Other properties could be obtained through the UserManager, although it requires an extra access to persistence.
In this way you would't depend on user authentication or HttpContext, but I would still check the authentication problem if it exists.
Upvotes: 1