In some use cases there is a security requirement for Auditing of User Login Activity, which is not covered by IdentityServer3.
As we used the ASP.NET Identity as User Store, it also possible to implement some sort of Auditing as a custom SignInManager.cs in ASP.NET Identity, but that would not cover all Login Features that are supported by IdentityServer3, which us great Framework by the way and I had much joy of using it and I look forward of using the latest port of IdentityServer on ASP.NET Core.
Searching for a best way to implement this, I found out two interesting Features in Framework:
- Logging (more for Development-time logging, that is produces a quite extensive output and not that great for Auditing)
- Events (much better for production-time environments, firing four types of Events and is configurable. More on Events in IdentityServer3 here)
First of all, we will extend our IdentityDbContext that we used for ASP.NET Identity User Storage with another Class called UserAudit.cs with all the columns I want to store:
[Table("UserAuditEvents")]
public
class
UserAudit
{
[Key]
public
int Id { get; private
set; }
[Required]
public
string UserId { get; private
set; }
[Required]
public
DateTimeOffset Timestamp { get; private
set; } = DateTime.UtcNow;
[Required]
public
EventTypes AuditEvent { get; set; }
public
string IpAddress { get; private
set; }
public
static
UserAudit CreateAuditEvent(string userId, EventTypes auditEventType, string ipAddress)
{
return
new
UserAudit { UserId = userId, AuditEvent = auditEventType, IpAddress = ipAddress };
}
}
For EventTypes Enum you will need to add using of Thinktecture.IdentityServer.Core.Events namespace. It contains Integer values for following Events: Success = 1, Failure = 2, Information = 3 and Error = 4.
Now that we have a class which represents the database table, we should add it to the entity framework IdentityDbContext:
public
class
LoginDbContext : IdentityDbContext<User, Role, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, ILoginDbContext
{
public
virtual
DbSet<UserAudit> UserAuditEvents { get; set; }
}
At this point, we have a new database table defined which needs to be physically created. We will do this by creating a migration and applying it to the database or if you have AutomaticMigrationsEnabled = true then you just have to update your database by running the "Update-Database" in Package Manager Console of your Project.
Now we have to implement our AuditEventService.cs as an Implementation of IEventService.cs.
public
class
AuditEventService : IEventService
{
private
readonly
LoginDbContext _db;
public AuditEventService(LoginDbContext dbContext)
{
_db = dbContext;
}
public
void Raise<T>(Event<T> evt)
{
var localLoginDetails = evt.Details as
LocalLoginDetails;
if (localLoginDetails != null)
{
EnBWUserAudit newAuditRecord = EnBWUserAudit.CreateAuditEvent(
localLoginDetails.LoginUserName,
evt.EventType,
HttpContext.Current.Request.UserHostAddress
);
_db.UserAuditEvents.Add(newAuditRecord);
_db.SaveChangesAsync();
}
}
}
As you can see, Raise<T> function will be called when any Event is fired and because we are implementing Login Auditing, I am only interested in Events.Details of type LocalLoginDetails as they contain all relevant information about the Login that we want to audit. Also, we are Injecting the DbContext into the Construct of the Service because we need it for storing our audit records. This Injection needs to be registered separated from our IEventService in order to work. Code for this in a minute.
So the final part is here! Now we need to change our Startup.cs Class and add few lines to our IdentityServerServiceFactory.
var factory = new
IdentityServerServiceFactory();
First we need to register our AuditEventService and then the DbContext to the IdentityServerServiceFactory in order for Dependency Injection to work:
factory.EventService = new
Registration<IEventService, AuditEventService>();
factory.Register(new
Registration<LoginDbContext>(resolver => new
LoginDbContext()));
Now, it should all be working if there was not one "small" thing: IEventService configurations stored in EventOptions Class is by default turned off for all types of events. We need to add EventOptions to our IdentityServerOptions in order to get Events fired at all.
My IdentityServerOptions looked at the end like this:
var eventOptions = new
EventsOptions()
{
RaiseSuccessEvents = true,
RaiseFailureEvents = true
};
Finally, we can try to Login to our Application. If all went good, you should be able to see something like this in you Database Table "UserAuditEvents":
Summing Up
This blog post hopefully demonstrates the initial steps that we can follow to quite easily implement the IEventService in IdentityServer3 and extend it with nice auditing feature. I expect to be refactoring and extending this code further as my requirements determine.
Posted
Sep 23 2016, 10:41 AM
by
Armin Kalajdzija