ASP.NET
Core   Identity Server 4   Event Sink  
 
  | 
Events represent higher level
  information about certain operations in IdentityServer. Events are structured
  data and include event IDs, success/failure information, categories and
  details. (From docs.identityserver.io) | 
We are going to
create a custom Event Sink which can save successful sign-in information into
memory cache.
Notice that the
event is fired on Auth Server application.
 
 
  
  
  
  
  
  
  
  
  
  
  
  
 
 
 
 
▋docker-openldap 1.2.4 (OpenLDAP
2.4.47)
▋ASP.NET Core 2.2.203
▋IdentityServer4 2.4.0
▋Nordes/IdentityServer4.LdapExtension
2.1.8
The source code
is on my Github. 
▋Enable raising events
Events are not
turned on by default, make sure enabling raising events are configured in the ConfigureServices
method.
▋Startup.cs :
ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
  
#region Identity Server
   var builder = services.AddIdentityServer(options =>
   {
      
options.Events.RaiseErrorEvents = true;
      
options.Events.RaiseInformationEvents = true;
     
 options.Events.RaiseFailureEvents = true;
      
options.Events.RaiseSuccessEvents = true;
  
});
  
#endregion
}
Since we will
use Memory Cache and Session later when implementing the Custom Event Sink, we
have to 
▋Add IMemoryCache into IServiceCollection for DI
▋Startup.cs : ConfigureServices
 public void ConfigureServices (IServiceCollection services) 
 {
        // … Skip
        services.AddMemoryCache();       
 }
▋Enable Session
▋Startup.cs:
Configure
 public void Configure (IApplicationBuilder app, IHostingEnvironment env) 
 {
        // … Skip
        app.UseSession();
        app.UseMvc ();
 }
▋ Raise event(s)
Identity Server
4 uses the DI service: IEventService, to raise event(s) as below.
Notice that we
cannot pass parameter while raising events, we save the given Token in Session
so that we can get it later in an Event Sink class.
▋LdapController.cs
Here is the
sample for raising the UserLoginSuccessEvent.
 [Route("api/[controller]")]
 [ApiController]
 public class LdapController : ControllerBase
    {
        private readonly ILdapUserStore userStore = null;
        private readonly IEventService events = null;
        private readonly IdentityServerTools tools = null;
        public LdapController(
            ILdapUserStore userStore,
            IEventService events,
            IdentityServerTools tools)
        {
            this.userStore = userStore;
            this.events = events;
            this.tools = tools;
        }
        [HttpPost("SignIn")]
        public async Task<IActionResult> SignIn([FromBody]LdapUser model)
        {
            // validate username/password against Ldap
            var user = this.userStore.ValidateCredentials(model.Username, model.Password);
            if (user != default(IAppUser))
            {
                // Response with authentication
cookie
                await this.HttpContext.SignInAsync(user.SubjectId, user.Username);
                // Get the Access token
                var accessToken = await this.tools.IssueJwtAsync(lifetime: 3600, claims: new Claim[] { new Claim(JwtClaimTypes.Audience, model.ApiResource) });
                // Save
Access token to current session
                this.HttpContext.Session.SetString("AccessToken", accessToken);
                // Write the Access token to
response
                await this.HttpContext.Response.WriteAsync(accessToken);
                // Raise
UserLoginSuccessEvent
                await this.events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
                return this.Ok();
            }
            else
            {
                return this.Unauthorized();
            }
        }
 }
Raising the
event(s), such as UserLoginSuccessEvent, will trigger any
Custom Event Sink that had already been added to IServiceCollection.
Next step we
will create a Custom Event Sink.
▋
Custom Event Sink
Create a new
Event Sink class, "UserProfileCacheSink", which implement IdentityServer4.Services.IEventSink.
▋UserProfileCacheSink.cs
 public class UserProfileCacheSink : IEventSink
 {
        public async Task PersistAsync(Event evt)
        {
            
        }
    }
First inject the
necessary services,
 public class UserProfileCacheSink : IEventSink
 {
        private IHttpContextAccessor httpContextAccessor = null;
        private readonly ICacheKeyFactory cacheKeyFactory = null;
        private readonly IMemoryCache cache = null;
        private readonly ILogger<UserProfileCacheSink> logger = null;
    
        public UserProfileCacheSink(IHttpContextAccessor httpContextAccessor, ICacheKeyFactory cacheKeyFactory, IMemoryCache cache, ILogger<UserProfileCacheSink> logger)
        {
            this.httpContextAccessor = httpContextAccessor;
            this.cacheKeyFactory = cacheKeyFactory;
            this.cache = cache;
            this.logger = logger;
        }
         
        public async Task PersistAsync(Event evt)
        {
            
        }
    }
Now we can
implement the PersistAsync method, which will be fired when raising
events.
The main logic
of PersistAsync
is to store the {Subject : JWT token} json string, for example { "jblin","xxxxxxxxxx"
}, into Memory Cache.
 public async Task PersistAsync(Event evt)
 {
            if (evt.Id.Equals(EventIds.UserLoginSuccess))
            {
                if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information)
                {
                    var httpContext = this.httpContextAccessor.HttpContext;
                    try
                    {
                        if (this.httpContextAccessor.HttpContext.Session.IsAvailable)
                        {
                            var session = this.httpContextAccessor.HttpContext.Session;
                            var user = this.httpContextAccessor.HttpContext.User;
                            var subject = user.Claims.Where(x => x.Type == "sub").FirstOrDefault()?.Value;
                            var token = session.GetString("AccessToken");
                            string cacheKey = this.cacheKeyFactory.UserProfile(subject);
                            _ = await this.cache.GetOrCreateAsync<JObject>(cacheKey, async entry =>
                            {
                                entry.SlidingExpiration = TimeSpan.FromSeconds(600);
                                string jsonStr = $"{{\"{subject}\":\"{token}\"}}";
                                return JObject.Parse(jsonStr);
                            });
                            // Check if the
cache exist
                            if (this.cache.TryGetValue<JObject>(cacheKey, out JObject tokenInfo))
                            {
                               Debug.WriteLine($"Cached: {tokenInfo.ToString()}");
                            }
                            
                        }
                    }
                    catch (Exception)
                    {
                    }
                }
                else
                {
                    this.logger.LogError($"{evt.Name} ({evt.Id}), Details: {evt.Message}");
                }
           }
 }
Last but very
important!
We have to
inject the Custom Event Sink, in other words, add the UserProfileCacheSink
into IServiceCollection.
So that raising
event(s) will also fires our Custom Event Sink.
▋Startup.cs:ConfigureServices                                                                               
 public void ConfigureServices (IServiceCollection services) 
 {
         // … Skip
         #region Custom sinks
         services.AddScoped<IEventSink, UserProfileCacheSink>();
         #endregion
 }
Result: The
information is stored in Memory Cache as following runtime log.