[SECURITY][PROGRAMMING]MVC5 with Clef Authentication

So, given some recent issues around security, I decided to finish something I’d been thinking about for awhile now. Integrating the Clef Two-Factor Authentication system with ASP.NET MVC5.

Two-Factor Authentication is a login method where you use two completely separate identifiers to login as a specific identity, for instance – you must use the correct “pair” of a bank card and PIN number to access a specific bank account. I’ve used (and currently use) several Two-Factor authentication services before, RSA Tokens, Google’s Authenticator App, etc etc… However, Clef has a “wow-factor” for users that the other applications/services miss the mark on.

Clef’s website offers assistance for people wanting to integrate their authentication system into applications, however, while searching, I couldn’t find any examples of this being done with Microsoft’s web frameworks (all the examples on Clef’s website are using open-source technologies). So, I decided to do some code-porting and now the MVC5 with Clef Authentication demo Application is up and running on our trusty TITUS PULLO server.

To use the Application you just simply register an account (only need an e-mail address and password for the demo app), and install the Clef Application on your mobile device. Once done you can click the “Log in with your phone” button and watch the magic of Clef!

Clef Login Button
Clef Login Button

So, for those with an interest in the programming side, here’s what you’ll need to do to make this available in your MVC5 application (bare in mind, this Demo Application isn’t something I’ve built to be enterprise-ready out of the box, but just a starter I’ve built to provide people with an interest/need something to start from. I’ll build an enterprise version for people/companies if you can’t create your own, but you’ll have to pay for my time to do it!)…

Integrating ASP.NET MVC5 with Clef Authentication….

1. Follow the Guide on the Clef website and create your Clef Application.

2. Generate the “Login with your phone” button script tags and add them into your _LoginPartial view in your MVC5 project (I modified mine to use configuration settings rather than hardcoded values).

<li><div class="clef-wrapper" style="padding-top:7px"><script type="text/javascript" data-state='@UtilityMethods.GenerateCSRFToken(Request.Cookies["BrowserId"].Value)' src='@System.Configuration.ConfigurationManager.AppSettings["ClefAppJSURL"].ToString()' class="clef-button" data-app-id='@System.Configuration.ConfigurationManager.AppSettings["ClefAppId"].ToString()' data-color="blue" data-style="button" data-redirect-url='@(new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port).ToString().TrimEnd("/".ToCharArray()) + Url.Content(System.Configuration.ConfigurationManager.AppSettings["ClefRedirectURL"].ToString()))' data-type="login"></script></div></li>

3. Add the methods below into your Global.asax file (it’s a basic form of Browser “Fingerprinting”)…

        protected void Application_PreSendRequestHeaders(object sender, EventArgs e)
        {
            if (!HttpContext.Current.Response.Cookies.AllKeys.Contains("BrowserId") 
                && !HttpContext.Current.Request.Url.ToString().ToLower().Contains("cleflogouthook")) // should not be set for the logout hook.
                HttpContext.Current.Response.Cookies.Add(HttpContext.Current.Request.Cookies["BrowserId"]);

            // Set the Clef Id in the Cookies if it's been set previously.
            if (!HttpContext.Current.Response.Cookies.AllKeys.Contains("clef_id")
                && HttpContext.Current.Request.Cookies.AllKeys.Contains("clef_id"))
                HttpContext.Current.Response.Cookies.Add(HttpContext.Current.Request.Cookies["clef_id"]);
        }

        protected void Application_BeginRequest(object sender, EventArgs e)
        { 
            if (!HttpContext.Current.Request.Cookies.AllKeys.Contains("BrowserId"))
                HttpContext.Current.Request.Cookies.Add(new HttpCookie("BrowserId", Guid.NewGuid().ToString())); //Standard Value
        }

4. Then create a couple of models for the purpose of storing token information. This could also be done in a singleton class and stored in memory, however as alot of web applications are clustered/proxied this will not suit for that purpose, so I created them as database objects which means you can cluster your web application(s) to your hearts content!

using System;
using System.Data.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MVC5ClefAuthentication.Models
{
    public class CSRFTokenModel
    {
        public int Id { get; set; }

        public Guid BrowserId { get; set; }

        public Guid CSRFToken { get; set; }

        public CSRFTokenModel() { }

        public CSRFTokenModel(Guid BrowserId, Guid CSRFToken)
        {
            this.BrowserId = BrowserId;
            this.CSRFToken = CSRFToken;
        }        
    }

    public class CSRFTokenModelsDBContext : DbContext
    {
        public DbSet<CSRFTokenModel> CSRFTokens { get; set; }
    }
}

using System;
using System.Data.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MVC5ClefAuthentication.Models
{
    public class LogoutTokenModel
    {
        public int Id { get; set; }

        public string ClefId { get; set; }
        
        public LogoutTokenModel() { }

        public LogoutTokenModel(string ClefId)
        {
            this.ClefId = ClefId;            
        }
    }

    public class LogoutTokenModelsDBContext : DbContext
    {
        public DbSet<LogoutTokenModel> LogoutTokens { get; set; }
    }
}

5. Add the necessary configuration lines to your web.config:

 <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-MVC5ClefAuthentication-20150425121809.mdf;Initial Catalog=aspnet-MVC5ClefAuthentication-20150425121809;Integrated Security=True"
      providerName="System.Data.SqlClient" />
    <add name="CSRFTokenModelsDBContext" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\CSRFTokenModelsDBContext.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
    <add name="LogoutTokenModelsDBContext" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\LogoutTokenModelsDBContext.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="ClefAppId" value="5f29efb37c1231231232875081b3596c" />
    <add key="ClefAppSecret" value="2098123456789009876543210d59ec8f" />
    <add key="ClefAppAuthURL" value="https://clef.io/api/v1/authorize" />
    <add key="ClefAppJSURL" value="https://clef.io/v3/clef.js" />
    <add key="ClefInfoURL" value="https://clef.io/api/v1/info" />
    <add key="ClefAppLogoutURL" value="https://clef.io/api/v1/logout" />
    <add key="ClefRedirectURL" value="~/Account/ClefRedirect"/>
  </appSettings>

6. Create the UtilityMethods class (used for token control, basically state-maintenance for the login/logouts, I decided to force users to logout of clef before logging back in to the application).

using System;
using System.Data.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using MVC5ClefAuthentication.Models;

namespace MVC5ClefAuthentication.Classes
{
    public class UtilityMethods
    {
        public static string GenerateCSRFToken(string BrowserId)
        {
            Guid newGuid = Guid.NewGuid();

            using (CSRFTokenModelsDBContext db = new CSRFTokenModelsDBContext())
            { 
                CSRFTokenModel token = new CSRFTokenModel(Guid.Parse(BrowserId), newGuid);
    
                db.CSRFTokens.Add(token);

                db.SaveChanges();
            }

            return newGuid.ToString();
        }

        public static void AddLogoutToken(string clef_id)
        {
            using (LogoutTokenModelsDBContext db = new LogoutTokenModelsDBContext())
            { 
                LogoutTokenModel token = new LogoutTokenModel(clef_id);
    
                db.LogoutTokens.Add(token);

                db.SaveChanges();
            }                   
        }

        public static bool CheckLogoutToken(string clef_id)
        {
            using (LogoutTokenModelsDBContext db = new LogoutTokenModelsDBContext())
            {
                LogoutTokenModel token = new LogoutTokenModel(clef_id);

                List<LogoutTokenModel> tokenList = db.LogoutTokens.ToList();

                for (int i = 0; i < tokenList.Count(); i++)
                    if (tokenList[i].ClefId == clef_id)
                    {
                        return true;
                    }
            }
            return false;
        }

        public static void RemoveLogoutToken(string clef_id)
        {
            using (LogoutTokenModelsDBContext db = new LogoutTokenModelsDBContext())
            {
                LogoutTokenModel token = new LogoutTokenModel(clef_id);

                List<LogoutTokenModel> tokenList = db.LogoutTokens.ToList();

                for (int i = 0; i < tokenList.Count(); i++)
                    if (tokenList[i].ClefId == clef_id)
                    {
                        db.LogoutTokens.Remove(tokenList[i]);
                        db.SaveChanges();
                    }
            }
        }

        public static bool ValidateCSRFToken(string BrowserId, string tokenGuid)
        {
            using (CSRFTokenModelsDBContext db = new CSRFTokenModelsDBContext())
            {
                CSRFTokenModel token = new CSRFTokenModel(Guid.Parse(BrowserId), Guid.Parse(tokenGuid));

                List<CSRFTokenModel> tokenList = db.CSRFTokens.ToList();

                for (int i = 0; i < tokenList.Count(); i++)
                    if (tokenList[i].BrowserId == token.BrowserId && tokenList[i].CSRFToken == tokenList[i].CSRFToken)
                    {
                        return true;
                    }
            }

            return false;
        }

        public static void RemoveCSRFToken(string BrowserId, string tokenGuid)
        {
            using (CSRFTokenModelsDBContext db = new CSRFTokenModelsDBContext())
            {
                CSRFTokenModel token = new CSRFTokenModel(Guid.Parse(BrowserId), Guid.Parse(tokenGuid));

                List<CSRFTokenModel> tokenList = db.CSRFTokens.ToList();

                for (int i=0;i<tokenList.Count();i++)
                    if (tokenList[i].BrowserId == token.BrowserId && tokenList[i].CSRFToken == tokenList[i].CSRFToken)
                    {
                        db.CSRFTokens.Remove(tokenList[i]);
                        db.SaveChanges();
                        return;
                    }                                
            }
        }
    }
}

7. Create the Redirect and LogoutHook Methods (I created them in the Accounts Controller):

        //
        // GET: /
        [AllowAnonymous]
        public async Task<ActionResult> ClefRedirect()
        {
            string access_token = String.Empty;
            string jsonresponse = String.Empty;

            if (Request.Cookies.AllKeys.Contains("clef_id")) // So the user is forced to login again via Clef.
            {
                if (!UtilityMethods.CheckLogoutToken(Request.Cookies["clef_id"].Value)) { 
                    TempData["Message"] = "Error, you have not logged out of Clef, please Logout of Clef before attempting to log back in!";
                    return RedirectToAction("ExternalLoginFailure", "Account");
                }
                else
                {
                    UtilityMethods.RemoveLogoutToken(Request.Cookies["clef_id"].Value);                              
                }
            }

            NameValueCollection queryStringCollection = HttpUtility.ParseQueryString(Request.Url.Query);

            string code = queryStringCollection.GetValues("code")[0];
            string state = queryStringCollection.GetValues("state")[0];

            if (!UtilityMethods.ValidateCSRFToken(Request.Cookies["BrowserId"].Value, state))
                return RedirectToAction("Index","Home");
            else
                UtilityMethods.RemoveCSRFToken(Request.Cookies["BrowserId"].Value, state);

            NameValueCollection postDataCollection = new NameValueCollection();
            
            postDataCollection.Add("code", code);

            postDataCollection.Add("app_id", System.Configuration.ConfigurationManager.AppSettings["ClefAppId"].ToString());

            postDataCollection.Add("app_secret", System.Configuration.ConfigurationManager.AppSettings["ClefAppSecret"].ToString());
            
            using (WebClient client = new WebClient())
            {                
                byte[] response = client.UploadValues(System.Configuration.ConfigurationManager.AppSettings["ClefAppAuthURL"].ToString(), postDataCollection);

                jsonresponse = Encoding.UTF8.GetString(response);
            }

            if (jsonresponse.Contains("\"success\": true")) { 
                access_token = jsonresponse.Split("\"".ToCharArray())[3];

            }
            else
                return RedirectToAction("Index","Home");

            string userInfo = String.Empty;

            using (WebClient client = new WebClient())
            {
                userInfo = client.DownloadString(System.Configuration.ConfigurationManager.AppSettings["ClefInfoURL"].ToString() + "?access_token=" + access_token);
            }

            ApplicationUser theUser = SignInManager.UserManager.FindByEmail(userInfo.Split("\"".ToCharArray())[5]);
            
            if (theUser != null)
            { 
                await SignInManager.SignInAsync(theUser, true, false);                
            }

            Response.Cookies.Add(new HttpCookie("clef_id", userInfo.Split("\"".ToCharArray())[8].TrimStart(new char[] {':', ' '}).Split("\n".ToCharArray())[0]));            
            
            return RedirectToAction("Index","Home");
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<string> ClefLogoutHook(string logout_token)
        {
            NameValueCollection postDataCollection = new NameValueCollection();

            postDataCollection.Add("logout_token", logout_token);

            postDataCollection.Add("app_id", System.Configuration.ConfigurationManager.AppSettings["ClefAppId"].ToString());

            postDataCollection.Add("app_secret", System.Configuration.ConfigurationManager.AppSettings["ClefAppSecret"].ToString());

            string message = String.Empty;
            string jsonresponse = String.Empty;            

            using (WebClient client = new WebClient())
            {
                byte[] response = client.UploadValues(System.Configuration.ConfigurationManager.AppSettings["ClefAppLogoutURL"].ToString(), postDataCollection);

                jsonresponse = Encoding.UTF8.GetString(response);
            }

            if (jsonresponse.Contains("\"success\": true"))
                message = "Success!";
            else
                message = "Error!";

            UtilityMethods.AddLogoutToken(jsonresponse.Split("\"".ToCharArray())[2].TrimStart(new char[] { ':', ' ' }).Split("\n".ToCharArray())[0].TrimEnd(",".ToCharArray()));

            return message;
        }

8. Modify your ExternalLoginFailure View/Controller to display an appropriate message for when user’s fail to logout of Clef after logging out of the application.

That’s pretty much it – I don’t think I’ve missed anything significant around how the application is built/integration is done. I’m happy to provide the actual solution file to those who want to see it (at a bargain price), just e-mail if you want a copy.

Thanks,

–Owen.

 

Leave a Reply