Adding a Secured Geo-located Audit Trail

Adding a Secured Geo-located Audit Trail

How I built a social sharing component for my own web site and added a secured geo-located audit trail. Step by step, let’s analyse technologies and source code for developing this component.

 

This is the second of two articles about adding social sharing capability to a web site, and auditing its usage. In this article I’ll focus on implementing a secured geo-located audit trail. If the first article, I introduced the sharing component.

 

Introduction

In my previous article I presented the share buttons implemented as an MVC component made of a ShareButtons action and view. I have introduced a dedicated MVC controller for the purpose, to keep action isolated. The Social controller contains the actions for displaying the view and auditing the use of the share buttons.

[ChildActionOnly]

public ActionResult ShareButtons(int id)

{

    using (var db = new DatabaseContext())

    {

        return PartialView(new

        {

            Article = db.Articles.Find(id),

            SecurityToken = Request.Url.Host.Encrypt(EncryptionKey, EncryptionIV)

        }.ToDynamic());

    }

}

The ShareButtons action in the Social controller.

 

This action generates a security token to pass to the view for validating subsequent Ajax calls for auditing purpose.

Let’s expand on securing the auditing functionality…

 

Security

How can we secure our auditing capability? We want to make sure that no-one else than the Share buttons can consume the Audit action. My implementation is based on encrypting the host name of the URL of the article page (where the Share buttons are displayed), and passing this encrypted string as a security token to the Share buttons. When the Ajax call is done from the Share buttons to the Audit action, as we will see later in detail, validation is done of the request URL against the token, and if they match, the audit request is accepted. This should prevent anything else outside of the current web page to invoke the Audit action.

The work flow is depicted in the following diagram.

Workflow to secure the Audit action.

 

Let’s expand on the Encrypt and Decrypt sub-processes. Both are implemented as String extensions. Starting with a string in clear (the Request URL host name, in our case), the Encrypt method:

1.     Uses the Triple DES symmetric-key algorithm for encryption, with Key and Vector defined in the Web config file.

<appSettings>

    <add key="EncryptionKey" value="1234567890ABCDEFGHJK!.$@"/>

    <add key="EncryptionIV" value="01234567"/>

</appSettings>

Encryption Key and IV (Vector) for the DES3 algorithm.

 

2.     Obtains a byte array from the clear message using the system’s default encoding.

3.     Creates a crypto stream in which the message is encrypted to.

4.     Copies the encrypted message into an in-memory message.

5.     Converts the byte array of the in-memory message into Base 64 to make it HTTP friendly (i.e. textual format, as opposite to the encrypted stream, which is binary).

The source code is:

public static string Encrypt(this string clearMessage, string key, string vector)

{

    byte[] message = Encoding.Default.GetBytes(clearMessage);

    string cipher = string.Empty;

 

    SymmetricAlgorithm des3 = SymmetricAlgorithm.Create("3DES");

    des3.Key = Encoding.Default.GetBytes(key);

    des3.IV = Encoding.Default.GetBytes(vector);

 

    using (MemoryStream mstream = new MemoryStream())

    {

        using (CryptoStream cstream = new CryptoStream(mstream, des3.CreateEncryptor(), CryptoStreamMode.Write))

        {

            cstream.Write(message, 0, message.Length);

        }

 

        byte[] cryptostream = mstream.ToArray();

        cipher = Convert.ToBase64String(cryptostream);

    }

 

    return cipher;

}

The Encrypt extension.

 

Likewise, the Decrypt method:

1.     Converts the encoded message from Base 64 to an encrypted byte array.

2.     Using a crypto stream in decrypting mode, decrypts the message into an in-memory stream.

3.     From the in-memory stream, obtains a byte array and convert it into a string using the system’s default encoding.

4.     As encrypted messages have length that is a multiple of the IV vector, trailing empty characters may result in the decrypted message (characters with ASCII code 0); these are trimmed out from the resulting string.

The source code is:

public static string Decrypt(this string encryptedMessage, string key, string vector)

{

    byte[] cipher = Convert.FromBase64String(encryptedMessage);

    string message = null;

 

    SymmetricAlgorithm des3 = SymmetricAlgorithm.Create("3DES");

    des3.Key = Encoding.Default.GetBytes(key);

    des3.IV = Encoding.Default.GetBytes(vector);

 

    using (MemoryStream mstream = new MemoryStream(cipher))

    {

        using (CryptoStream cstream = new CryptoStream(mstream, des3.CreateDecryptor(), CryptoStreamMode.Read))

        {

            byte[] clearstream = new byte[cipher.Length];

            cstream.Read(clearstream, 0, cipher.Length);

 

            message = Encoding.Default.GetString(clearstream).TrimEnd('\0');

        }

    }

 

    return message;

}

The Decrypt extension.

 

Auditing

Implementing auditing requires a full stack client – server – database, so with the same logic of putting the user experience first in our design, let’s start with the jQuery event handler of the “on click” event on the share buttons, which, I recall, are identified by the “social” CSS class.

After obtaining the value of social and src from the respective data- attributes as local JavaScript variables, the component makes an Ajax call to the Audit action on the Social controller. This call is secured by adding the security token to the header, and it passes modelId (the Id of the article) and socialName (the name of the social network) as data.

 

$(".social").on("click", function () {

    var social = $(this).data("social");

    var id = $(this).parent().parent().data("id");

 

    $.ajax({

        url: "@Url.Action("Audit", "Social")",

        type: "POST",

        headers: { securityToken: "@Model.SecurityToken"},

        data: { modelId: id, socialName: social }

    });

});

jQuery event handler for auditing, when clicking on a share button.

 

On the server site, the Audit action accepts modelId and socialName in input and returns an HTTP status code. More precisely, this action is marked as async so it can run asynchronously when accessing the database to add the information to the audit trail.

A couple of checks before the actual code for storing the audit data:

1.     The action expects a header parameter with key “securityToken”; if it doesn’t exists, it returns the HTTP error code 400 “Bad Request”.

2.     If it exists, the security token is decrypted and the clear phrase compared with the host name of the request, to make sure that the request is genuine; if there is no match, the HTTP error code 401 “Unauthorized” is returned.

Passing these validation controls, a new instance of the AuditTrailEntry model is created with several pieces of information collected from the request. This data is then added to the audit trail via Entity Framework, and saved asynchronously with the SaveChangeAsync method.

If all goes well at the end, the HTTP status code 200 “OK” is returned.

[HttpPost]

public async Task<HttpStatusCodeResult> Audit(int modelId, string socialName)

{

    string securityToken = Request.Headers["securityToken"];

    if (string.IsNullOrEmpty(securityToken))

    {

        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

    }

 

    string phrase = securityToken.Decrypt(EncryptionKey, EncryptionIV);

    if (phrase != Request.Url.Host)

    {

        return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);

    }

 

    var entry = new AuditTrailEntry

    {

        TimeStamp = DateTime.Now,

        Browser = Request.Browser.Browser,

        BrowserVersion = Request.Browser.Version,

        BrowserMajorVersion = Request.Browser.MajorVersion,

        BrowserIsMobileDevice = Request.Browser.IsMobileDevice,

        BrowserPlatform = Request.Browser.Platform,

        UrlReferrer = Request.UrlReferrer?.ToString(),

        UserAgent = Request.UserAgent,

        UserHostAddress = Request.UserHostAddress,

        SocialName = socialName,

        ModelId = modelId

    };

 

    using (var db = new DatabaseContext())

    {

        db.AuditTrail.Add(entry);

        await db.SaveChangesAsync();

    }

 

    return new HttpStatusCodeResult(HttpStatusCode.OK);

}

The Audit action in the Social controller.

 

On the backend, the AuditTrailEntry model is a POCO class that exposes public properties for each piece of information to capture. I marked it with the Table attribute, which is a data annotation to force the name of the table at database label. If I didn’t specify the “AuditTrail” name for the table, EF would pluralise the model name into AuditTrailEntries. Which is fine, but I wanted my table to be called AuditTrail J

[Table("AuditTrail")]

public class AuditTrailEntry

Setting a table name with the Table attribute.

 

The schema of the AuditTrail class is the following. Feel free to improve it at your wish!

The AuditTrail table schema.

 

Geolocation

We are nearly at the end! We are now able to share our articles on different social network sites, and this action is audited. Important information is collected in our audit trail. A useful attribute is the UserHostAddress. This is the IP address of the client request. IP addresses can be geo-located, that is it is possible to know from which country they originated. This can be useful for statistical purposes, for example.

There are different services that offer geo-resolution of an IP address. I am using “IP Info.io”, available at http://ipinfo.io/. This service is free for small projects, up to 1000 daily requests, and has paid plans for larger numbers of daily requests. It also offers a very convenient JSON API.

So all I need is a way to consume a JSON API from my Audit action. The best way to do that in .NET is to use the Microsoft.AspNet.WebApi.Client library, which adds support for formatting and content negotiation to the System.Net.Http namespace. Using NuGet for adding this reference to the project, this will also install the Newtonsoft.Json package, which contains Json.NET, a popular high-performance JSON framework for .NET.

My implementation of the geolocation capability is based on an IGeolocator interface that exposes a Geolocate method, and an actual implementation of the IP Info.io client in the IpInfoGeolocator class. In this way, by using the interface as a contract, I would not need to change the code that implements the update of the country field in the audit trail entry, if I had to replace IP Info.io with another provider.

Geolocator class diagram.

 

The IGeolocator interface is straightforward. It exposes a Geolocate method that accepts an IP address in input and returns a GeolocationInfo model. Actually, a Task<GeolocationInfo>. This is so we can invoke Geolocate asynchronously.

public interface IGeolocator

{

    Task<GeolocationInfo> Geolocate(string ipAddress);

}

The IGeolocator interface.

 

GeolocationInfo is a class that defines the geographical information that is possible to obtain as part of the geolocation resolution of an IP address. Accurate services may be able to track an IP address down to a specific location within a city. For our purposes, we limit our search at country level.

public class GeolocationInfo

{

    public string Ip { get; set; }

 

    public string Loc { get; set; }

 

    public string City { get; set; }

 

    public string Region { get; set; }

 

    public string Country { get; set; }

}

The GeolocationInfo model.

 

The implementation of the IGeolocator interface is in the IpInfoGeolocator, which, as said, implements the geolocation services of the IP Info.io provider.

public class IpInfoGeolocator : IGeolocator

{

    public IpInfoGeolocator(string geolocationApiUrl)

    {

        GeolocationApiUrl = geolocationApiUrl;

    }

 

    public string GeolocationApiUrl { get; }

 

    public async Task<GeolocationInfo> Geolocate(string ipAddress)

    {

        using (var httpClient = new HttpClient { BaseAddress = new Uri(GeolocationApiUrl) })

        {

            httpClient.DefaultRequestHeaders.Accept.Clear();

            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

 

            HttpResponseMessage response = await httpClient.GetAsync($"/{ipAddress}/geo");

            if (!response.IsSuccessStatusCode)

            {

                return null;

            }

 

            return await response.Content.ReadAsAsync<GeolocationInfo>();

        }

    }

}

The IpInfoGeolocator class.

 

By using an HttpClient object, the Geolocate method invokes the “geo” endpoint by submitting the IP address, and obtains a GeolocationInfo result as a response. Everything is happening asynchronously to prevent hanging this call whilst the web service processes the request.

I also avoided hard-coding the base URL of the geolocation API, so that address is stored in the Web.config and passed in input to IpInfoGeolocator from the Audit action.

<appSettings>

    <add key="GeolocationApiUrl" value="http://ipinfo.io"/>

</appSettings>

 

All the Audit action has to do is to trigger the geolocation resolution after saving the audit entry in the database. This is implemented as an extension on the AuditTrailEntry model, which, if you remember, is the class that defines all the attributes to audit.

await entry.GeolocateIpAddress(new IpInfoGeolocator(GeolocationApiUrl));

 

The extension is defined as follows:

public async static Task GeolocateIpAddress(this AuditTrailEntry entry, IGeolocator geolocator)

{

    using (var db = new DatabaseContext())

    {

        var info = await geolocator.Geolocate(entry.UserHostAddress);

        if (info != null)

        {

            entry.UserCountry = info.Country;

            db.Entry(entry).State = EntityState.Modified;

        }

 

        await db.SaveChangesAsync();

    }

}

Extension to geolocate the IP Address of an audit entry.

 

The GeolocateIpAddress accepts an IGeolocator interface in input to inject the dependency on which geolocation provider to use. Then it obtains the country of the IP address of the client request, and lastly it updates the audit entry. Again, all happens asynchronously to avoid hanging this process and instead returning control to the web page immediately.

 

Conclusions

Full source code of the solution is on CodePlex. Feel free to use it, it’s completely open source! Enjoy! J

 

 

 


  Comments

 

 Source Code

Project Name: SocialSharing


 Related Content
A flexible Default Value for your DateTime properties
When creating an MVC application with Entity Framework, it is possible to set default values for most properties in a model using the DefaultValue attribute. However, no much flexibility is offered for a DateTime property. This article presents a custom validation attribute for DateTime types that accepts different formats for defining the default value of the property.
Adding Social Sharing to a Web Site
How I built a social sharing component for my own web site and added a secured geo-located audit trail. Step by step, let’s analyse technologies and source code for developing this component.
Best practices for mobile form design in SharePoint
Build effective SharePoint forms with Nintex that are accessible anywhere, at any time, and on any device. You built the workflows, you built the forms, now make them mobile.
Bring your “A” game to ESPC16
With just over 3 weeks to go to Europe's largest gathering of SharePoint & Office 365 professionals, take a look at these tips that will help you get the most out of ESPC16…
Building an MVC application for SharePoint
Learn how to write code to perform basic operations with the SharePoint 2013 .NET Framework client-side object model (CSOM), and build an ASP.NET MVC application that retrieves information from a SharePoint server.
CIO vs CTO
What are the synergies and differences of the roles of a Chief Information Officer and a Chief Technology Officer? An open conversation about two roles with one mission…
Coded UI test automation of MVC applications with Visual Studio
Whether you are a software developer, tester, administrator or analyst, this article can help you master different types of UI testing of an MVC application, by using Visual Studio for creating coded UI test suites that can be automated for continuous execution.
Converting GIS spatial coordinates
Different formats and standards exist for describing geographical coordinates in GIS systems and applications. This article explains how to convert between the most used formats, presenting a working library written in C#.
Creating mobile accessible forms in SharePoint
With the release of the Nintex Mobile apps, SharePoint users can now optimise their experience across popular mobile devices and platforms.
Define your Performance Testing strategy with Visual Studio
Performance Testing is an essential part of software testing, with the specific goal of determining how a system performs in terms of responsiveness and stability under a particular workload. In this series of posts we’ll define and execute a good strategy for testing performance of an application using Visual Studio.
Disserting about colliding GUIDs and the Big Bang Theory
Can you generate two identical GUIDs? Would the world end if two GUIDs collide? How long does it take to generate a duplicate GUID and would we be still here when the result is found?
GIS Location Services in Dynamics CRM
A design paper about implementing GIS-based services for a local Council in Dynamics CRM, structuring address data, and delivering location services in the form of WebAPI endpoints via an Enterprise Service Bus.
Group or Team?
All teams are groups but not all groups are teams. What defines a group and what a team? When do we need one over the other one?
How to Give Constructive Feedback
Learning to give and receive constructive feedback is an essential part of learning, growing, improving and achieving our goals.
Mirroring an iPad on your laptop
Have you ever wanted to see your iPhone or iPad on a larger screen? Play games, watch movies, demo apps or present to your computer from your iPhone or iPad. Reflector mirrors iOS devices on big screens wirelessly using iOS built-in AirPlay mirroring.
Mobilize your SharePoint workflows
Build workflow applications in SharePoint that can be accessed on mobile devices using the Nintex solution for business process mobilization.
Natural String Sorting
Have you ever desired to have in your code a way to order a sequence of strings in the same way as Windows does for files whose name contains a mix of letters and numbers? Natural string sorting is not natively supported in .NET but can be easily implemented by specialising a string comparer and adding a few extensions to the enumerable string collection.
Sales Effectiveness in Dynamics CRM with Azure IoT and Machine Learning - Part 1
How can an organisation optimise its sales channels and product targeting by building a 365-degree view of its customers in Dynamics CRM? The answer, and topic of this article, is with the help of Azure IoT and Machine Learning services!
Scaling Applications with Azure Redis and Machine Learning - Part 1
This article presents design best practices and code examples for implementing the Azure Redis Cache and tuning the performance of ASP.NET MVC applications, optimising cache hit ratio and reducing “miss rate” with smart algorithms processed by Machine Learning.
Software Development Management in 11 steps
What it takes to be a great Software Development Manager? I have been building software for the last 15 years and have collected a few stories and experiences to share. Some I have used as questions when interviewing candidates. In 11 points, this is my story to date.
SOLID SharePoint apps with MVC
Practical code examples of ASP.NET MVC applications that connect to a SharePoint Server and comply with the SOLID principles.
The art of outsourcing projects
Outsourcing may look financially attractive, but working with companies in far-off lands introduces challenges that, if not considered properly, can drive any project to failure. Let’s explore some common pitfalls when working with offshore partners and a common-sense approach to work around them.
The value of hashtags
Customers expect a modern approach to advertising. Digital advertising can leverage evolving technology to provide just-in-time, just-at-the-right-place promotions.
We don't need no Yoda's syntax
There is an urban myth in the programmers’ community that the so called “Yoda’s syntax” performs better when checking an object for nullity. Let's demystify it...