One of the most important aspects of the modern internet world is security so we have recently incorporated two-factor authentication in our open source code for Telligent, in this article I'll summarize how we achieved this.

We decided to use the Google Authenticator app but this could easily be substituted for other providers or an sms based solution if required using the same basic principles. If you are security aware, you may already have the Google Authenticator app installed on your smartphone, using it for two-factor authentication on Gmail/Dropbox/Lastpass/Amazon etc. The two-factor authentication requirement can be enabled on a per-user basis. You could enable it for your administrator account, but log in as usual with less privileged accounts.

As with all of our projects all of the code I discuss here can be found on GitHub.

https://github.com/4-Roads/FourRoads.TelligentCommunity/tree/Telligent10/src/code/FourRoads.TelligentCommunity.GoogleMfa for this one.  There is also a nuget feed here so you can download and use the plugin without an coding.

There are 2 main components to this solution, configuration and authentication.

Configuration

The first requirement is to provide the ability for the user to activate and configure two-factor authentication, we decided to build this into a widget "4 Roads - Google MFA Configuration" which can be added to the user account settings page, the widget provides the ability to activate, configure and disable two-factor authentication. Users will also need to install the google authenticator app on their mobile device/desktop but I won't go into that in detail here as there are many resources of help on the web.

The configuration process starts with a request to google to obtain a unique code that the user registers within their authenticator app, we are using a 3rd party module https://github.com/brandonpotter/GoogleAuthenticator for the integration with google.

public SetupInfo GenerateSetupInfo()
{
    var userService = Apis.Get<IUsers>();
    var groupsService = Apis.Get<IGroups>();
    
    var user = userService.AccessingUser;
    
    if (user.Username != userService.AnonymousUserName)
    {
        TwoFactorAuthenticator tfa = new TwoFactorAuthenticator();
        
        var setupInfo = tfa.GenerateSetupCode(groupsService.GetRootGroup().Name, user.PrivateEmail, user.ContentId.ToString(), 300, 300);
        
        return new SetupInfo() {ManualEntrySetupCode = setupInfo.ManualEntryKey, QrCodeImageUrl = setupInfo.QrCodeSetupImageUrl};
    }
    
    return null;
}


The URL for the QR code and the manual entry value are passed to the widget for rendering, the manual entry value is important as the user may not have the ability to scan the QR code depending on the authenticator solution they are using.

#set($twoFactor = $frcommon_v1_googleMfa.GenerateSetupInfo())

<div id="$core_v2_encoding.HtmlAttributeEncode($configure)">
    <p>$core_v2_language.GetResource('Introduction')</p>
    <div><img src="$core_v2_encoding.HtmlAttributeEncode($twoFactor.QrCodeImageUrl)" /></div>
    <div><label>$core_v2_language.GetResource('Manual_Code')</label>$twoFactor.ManualEntrySetupCode</div>
    
    <div class="form">
        <p>$core_v2_language.GetResource('Complete_Setup')</p>
        <fieldset>
            <ul class="field-list">
                <li class="field-item display-name">
                    <label for="$core_v2_encoding.HtmlAttributeEncode($displayNameId)">$core_v2_language.GetResource('VerifyCode')</label>
                    <span class="field-item-input"><input type="text" size="30" maxlength="254" id="$core_v2_encoding.HtmlAttributeEncode($validateInput)" name="$core_v2_encoding.HtmlAttributeEncode($validateInput)" /></span>
                    <span class="field-item-validation" style="display: none;">$core_v2_language.GetResource('InvalidCode')</span>
                </li>
                <li class="field-item">
                    <a class="button" id="$core_v2_encoding.HtmlAttributeEncode($submit)" href="javascript:void(0)">$core_v2_language.GetResource('VerifyAndEnable')</a>
                </li>
            </ul> 
        </fieldset> 
    </div>
</div>

Once the code has been registered with the authenticator app the user must verify the connection by entering the 6 digit verification number generated by the authenticator app back into the browser. This code is used to confirm that the application and the authenticator are in sync and if so the user account is updated to reflect that two-factor authentication is active.

#if($frcommon_v1_googleMfa.Validate($validationCode))
    $frcommon_v1_googleMfa.EnableTwoFactor(true)
    {"result":"true"}
else
    $frcommon_v1_googleMfa.EnableTwoFactor(false)
    {"result":"false"}
#end

Authentication

Now that a user has been configured for two-factor authentication we need to turn our attention to the login process itself and how we integrate two-factor authentication into the core Telligent login process. We need to intercept when a user has successfully logged into the site and if necessary redirect them to a secondary verification screen.

To achieve this we added a handler to a couple of events raised by the IUser service.

public MfaLogic(IUsers usersService, IUrl urlService)
{
    _usersService = usersService;
    _urlService = urlService;
    
    _usersService.Events.AfterIdentify += EventsAfterIdentify;
    _usersService.Events.AfterAuthenticate += EventsOnAfterAuthenticate;
}

AfterAuthenticate

This event is used to trap that the user has completed the regular login cycle, here we determine if two-factor authentication is active for the current user.  n.b. This code will work with the OOTB authentication system, but it has not been tested with oAuth or other authentication methods such as SSO Cookie.

private void EventsOnAfterAuthenticate(UserAfterAuthenticateEventArgs userAfterAuthenticateEventArgs)
{
    //user has authenticated
    //is 2 factor enabled for user?
    var user = _usersService.Get(new UsersGetOptions() {Username = userAfterAuthenticateEventArgs.Username});
    if (TwoFactorEnabled(user))
    {
        var request = HttpContext.Current.Request;
        if (request.Url.Host.ToLower() == "localhost" && request.Url.LocalPath.ToLower() == "/controlpanel/localaccess.aspx")
        {
            //bypass mfa for emergency local access
            SetTwoFactorState(user, true);
        }
        else
        {
            //Yes set flag to false
            SetTwoFactorState(user, false);
        }
    }
    else
    {
        //no set flag to true
        SetTwoFactorState(user, true);
    }
}

AfterIdentify

This event is used to trap that the user is attempting to access site content, here we check if two-factor authentication needs to be completed and if appropriate redirect the user to a verification screen.

Please note that this event gets raised on every request after Telligent has identified who the accessing user is. That may be the HTML page itself, as well as requests for any resources the page may load (CSS, JavaScript, images), AJAX requests and socket requests. And yes, it also gets fired even if the Accessing user is anonymous. So care needed to be taken to control when the user is prompted for the auth code and ensure that content is not passed through to the browser whilst we are in this second phase of authentication.

n.b. This code assumes that the site is at the root and may need changing to support sub application paths.


protected void EventsAfterIdentify(UserAfterIdentifyEventArgs e)
{
    var user = _usersService.AccessingUser;
    if (user.Username != _usersService.AnonymousUserName)
    {
        if (TwoFactorEnabled(user) && TwoFactorState(user) == false)
        {
            // user is logged in but has not completed the second auth stage
            var request = HttpContext.Current.Request;
            
            if (request.Path.StartsWith("/socket.ashx"))
            {
                return;
            }
    
            var response = HttpContext.Current.Response;
    
            // suppress any callbacks re search, notifications, header links etc
            if (request.Path.StartsWith("/api.ashx") ||
                (request.Url.LocalPath == "/utility/scripted-file.ashx" && 
                request.QueryString["_cf"] != null &&
                request.QueryString["_cf"] != "logout.vm" && 
                request.QueryString["_cf"] != "validate.vm"))
            {
                response.Clear();
                response.End();
            }
    
            // is it a suitable time to redirect the user to the second auth page
            if (response.ContentType == "text/html" &&
                !request.Path.StartsWith("/tinymce") && 
                request.Url.LocalPath != "/logout" &&
                request.Url.LocalPath != "/mfa" && 
                string.Compare(HttpContext.Current.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase) == 0 && 
                //Is this a main page and not a callback etc 
                (request.CurrentExecutionFilePathExtension == ".aspx" ||
                request.CurrentExecutionFilePathExtension == ".htm" ||
                request.CurrentExecutionFilePathExtension == string.Empty))
            {
                //redirect to 2 factor page
                response.Redirect("/mfa" + "?ReturnUrl=" + _urlService.Encode(request.RawUrl), true);
            }
        }
    }
}

The final part of the jiqsaw was to create a simple verification page which allows the user to enter the 6 digit verification number generated by the authenticator app back into the site and complete the two-factor authentication process by validating the code with google.

The verification process was implemented as another widget and the associated page registered within the plugin RegisterUrls method.

The verification widget simply obtains the verification code from the user and confirms it's validity with google, once complete the user is flagged as being fully authenticated and access to the site is allowed.

public bool Validate(string code)
{
    var userService = Apis.Get<IUsers>();
    var user = userService.AccessingUser;
    
    if (user.Username != userService.AnonymousUserName)
    {
        return Injector.Get<IMfaLogic>().ValidateTwoFactorCode(user, code);
    }
    
    return false;
}


Summary

The project hopefully demonstrates how you can incorporate two-factor authentication and could be used as a template to integrate verification via sms using Trillo or any other 3rd party authentication process. 4 Roads Google Mfa plugin is just another solution built to help customers get the most out of the Telligent platform, contact us to learn more on how we can help you with your community.

Anonymous