Search code examples
authenticationidentityserver4blazor

Blazor webassembly - IdentityServer EventSink and HttpContext


I'm working on a Blazor webassembly application, in ASP.NET Core 3.1 with IdentityServer. As IdentityServer handle all login, logout, register, ... events, I'm trying to catch theses events in order to obtain some informations about users.

To be clear, I'm trying to memorize the login Date, and to catch new users registration to give them some role automatically.

Here all the services I use in my startup class :

        services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

        services.Configure<IdentityOptions>(options =>
            options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);

        services.AddIdentityServer()
            .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
                options.IdentityResources["openid"].UserClaims.Add("name");
                options.ApiResources.Single().UserClaims.Add("name");
                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");

        services.AddAuthentication()
            .AddIdentityServerJwt();

I've implemented the IEventSink pattern (http://docs.identityserver.io/en/stable/topics/events.html) :

public class IdentityServerEventSink : IEventSink
{
    private readonly UserManager<ApplicationUser> userManager;
    private readonly IHttpContextAccessor httpContextAccessor;

    public IdentityServerEventSink(UserManager<ApplicationUser> userManager, IHttpContextAccessor httpContextAccessor)
    {
        this.userManager = userManager;
        this.httpContextAccessor = httpContextAccessor;
    }

    public async Task PersistAsync(Event @event)
    {
        if (@event.Id.Equals(EventIds.ClientAuthenticationSuccess))
        {
            var identity = httpContextAccessor.HttpContext.User;
            var id = httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
            var user = await userManager.Users.FirstOrDefaultAsync(u => u.Id == id);
        }
    }
}

And in the startup.cs :

services.AddHttpContextAccessor();
services.AddTransient<IEventSink, IdentityServerEventSink>();

But when I get in the ClientAuthenticationSuccess event, the identity is always anonymous.

I tried also in the middleware but I have the same issue :

            app.Use(async (context, next) =>
        {
            await next.Invoke();
            //handle response
            //you may also need to check the request path to check whether it requests image
            if (context.User.Identity.IsAuthenticated)
            {
                var userName = context.User.Identity.Name;
                //retrieve uer by userName
                using (var dbContext = context.RequestServices.GetRequiredService<ApplicationDbContext>())
                {
                    var user = dbContext.Users.Where(u => u.UserName == userName).FirstOrDefault();
                    user.LastLogon = System.DateTime.Now;
                    dbContext.Update(user);
                    dbContext.SaveChanges();
                }
            }
        });

Do you have any ideas ? I heard HttpContextAccessor is a bad things in Blazor.


Solution

  • Ok so after an update the middleware delegate works !

    app.Use(async (context, next) =>
    {
        await next.Invoke();
        if (context.User.Identity.IsAuthenticated)
        {
            var userName = context.User.Identity.Name;
            using (var dbContext = context.RequestServices.GetRequiredService<ApplicationDbContext>())
            {
                var user = dbContext.Users.Where(u => u.UserName == userName).FirstOrDefault();
                if (user != null)
                {
                    user.LastLogon = System.DateTime.Now;
                    user.LastIpAddress = context.Connection?.RemoteIpAddress?.ToString();
    
                    dbContext.Update(user);
                    dbContext.SaveChanges();
                }
            }
        }
    });
    

    Just be carefull to the order of app.use... in the Configure method ! You need to add this AFTER app.UseIdentityServer(); app.UseAuthentication(); app.UseAuthorization();

    But badly the EventSink from IdentityServer still doesn't work, the httpContextAccessor.HttpContext.User.Identity is always anonymous.