Tuesday, February 5, 2013

Getting the login faulire cause for TMG login using a sharepoint 2010 application page

Microsoft Forefront Threat Management Gateway (Forefront TMG), formerly known as Microsoft Internet Security and Acceleration Server (ISA Server), is a network security and protection solution for Microsoft Windows, described by Microsoft as "enables businesses by allowing employees to safely and productively use the Internet for business without worrying about malware and other threats"

SharePoint sites comprise one of the more common types of content that are secured by the Forefront Edge line. This stems from the critical need to provide remote document management while at the same time securing that access. Although Forefront UAG is the preferred solution for reverse proxy of a SharePoint environment, the Forefront TMG product is also a highly capable product that allows for reverse proxy functionality.

The login page behavior for the TMG server on an invalid login trial is only to inform the user that either the user name or password is incorrect, so the user keeps trying till his account becomes locked, Even the invalid trial would be of an expired password a locked account , the user name is wrong or the password is actually wrong .

You may have for you business need to specify to the user why did this trial fail.

Ok, I do not have any experience in  TMG, but I needed to get a workaround to solve this issue, I found that the login page for TMG is rendered from a HTML file under my TMG rule folder this file is usr_pwd.htm. a lot of people is talking about customizing this file for branding and applying your corporate look and feel style to this page, But how about changing its behavior to redirect to another page when the user fails to login to specify the  failure cause.

And here is the solution I managed to find - This may be not the best solution based on my limited experience in TMG but it works fine this is what we all care about in the end :) -

The game play plan is as the follwing:
-Once the user clicks the "Sign in" button, we will save his user name in a cookie.
-The sign in button causes page post back, So in the the page load we will check if the login was not successful, then we will redirect the user to an SharePoint application page , providing this page with the cookie reserved user name in the page query string
-In the application page , using some LDAB methods we will be able to find out why the user was not able to login by checking the following:
  1. First check the Lock Out Time property for the user if it is not "0" so the account is locked
  2. Second check the password last set property if it is "0" so the password was not ever set by the user
  3. Check the max password age property vs. the password last set property, if the time span between them both is less than zero then the password has expire 
  4. Else check the user account control property to get the current status of the account .
Lets start some coding....

- "Sign in" button, we will save his user name in a cookie
 To achive this we will edit the file "usr_pwd.htm" its path would be something like "C:\Program Files\Microsoft Forefront Threat Management Gateway\Templates\CookieAuthTemplates\{YOUR FOLDER NAME}\HTML\usr_pwd.htm"

Using some helper JavaScript methods already used in the "flogon.js" file , we will add the JavaScript functions that will create the UserName cookie and get this cookie and check wither the cookies is enabled or not

 

     function setUsernameCookie(){
        var userName = getUser().value;
        if (chkCookies()) {
        document.cookie="";
        setCookie("UserName",userName,20*1000);
        }
        else{
        alert("Cookies not enabled");
        }
    }
    function setCookie(c_name,value,exdays){
        var exdate=new Date();
        exdate.setDate(exdate.getDate() + exdays);
        var c_value=escape(value) + ((exdays==null) ? "" : "; expires="+exdate.toUTCString());
        document.cookie=c_name + "=" + c_value;
    }
    
    function getCookie(c_name){
        var i,x,y,ARRcookies=document.cookie.split(";");
        for (i=0;i<ARRcookies.length;i++){
          x=ARRcookies[i].substr(0,ARRcookies[i].indexOf("="));
          y=ARRcookies[i].substr(ARRcookies[i].indexOf("=")+1);
          x=x.replace(/^\s+|\s+$/g,"");
          if (x==c_name)
            {
            return unescape(y);
            }
          }
    }
As you see there is some JavaScript functions used like "getUser()" and "chkCookies()" that are already used in the "flogon.js" so we will just reuse them - We will not reinvent the wheel :)-

Now we need to bind the "setUsernameCookie()" function to the onclick event that is easy , Find the input element that have the id "SubmitCreds" and add the onClick attribute as follow onClick="setUsernameCookie()"

Ok step one save user name in a cookie DONE.

- On page load check if the login was not successful, then redirect to a SharePoint application page

Now the user had clicked sign in, the cookie is filled with his user name, but how we will know it was an unsuccessful trial, This is the worst part that was made in the unprofessional way  :(, but this how I managed to do it , I checked the td element that the login failure message will show in , Give it id , and in the page load checked if the message is the failure message then redirected the user .

After some elements inspection I found that the failure message is in the td element with class="wrng" and the inner text is @@INSERT_USER_TEXT so i found it in the html and gave it the following id tdLoginMessage, the result will be :
  


<td class="wrng" id="tdLoginMessage">@@INSERT_USER_TEXT</td>

Now in the page load if the tdLoginMessage contains the failure message redirect to an application page with the user name supplied in the page query string , AND YOU WILL BE BACK TO HE SHAREPOINT WORLD FINALLY :)



function window_onload() {
        onld();
        if(gbid("tdLoginMessage").innerText.indexOf("عذّر تسجيل دخولك إلى النظام. تأكد من صحة اسم المجال، واسم المستخدم، وكلمة المرور، ثم حاول من جديد") != -1){
            
            var userName = getCookie("UserName");
            
            if( userName != ""){
            window.location = "http://yoursever.local/_layout/Public/ValidateLoginFailure.aspx?UserName=" + userName;
            }
        }
        
        if (chkCookies()) {
            ldCookie('username', 'password');
 
            var expl1 = document.getElementById('expl1');
            expl1.style.display = "";
 
            var lnkHidedSection = document.getElementById('lnkHdSec');
            lnkHidedSection.style.display = "none";
 
            var lnkShowSection = document.getElementById('lnkShwSec');
            lnkShowSection.style.display = "";
        }
        
        
    }
In my case my message was in this arabic format and the application page created was ValidateLoginFailure.aspx

-Find out why the user was not able to login
Back to happy land, Now we will create an application page with the following properties:
-Map your page to a public folder where you have the anonymous rule on ISA enabled for this path as this page will be accessible by anonymous user - The guy faild to login he is still anonymous :) -
-Override the AllowAnonymousAccess property to return true to be able to be viewable by anonymous users



protected override bool AllowAnonymousAccess
{
    get
    {
        return true;
    }
}
Add the following to youe web.config to enable the page for anonymous

  <location path="_layouts/Public/ValidateLoginFailure.aspx">
    <system.web>
      <identity impersonate="true" />
      <authorization>
        <allow users="?" />
      </authorization>
    </system.web>
  </location>
Ok, in the page load we will get the user name and using the LDAP query we will get the login failure.

Now you should be redirected from the TMG login page with a URL looking like :  http://yoursever.local/_layout/Public/ValidateLoginFailure.aspx?UserName=Domain\LoginName

We will get the DirectoryEntry object for this user to be able to get the set of properties needed to know the failure cause by the following code:
  1. First check the Lock Out Time property for the user if it is not "0" so the account is locked
  2. Second check the password last set property if it is "0" so the password was not ever set by the user
  3. Check the max password age property vs. the password last set property, if the time span between them both is less than zero then the password has expire 
  4. Else check the user account control property to get the current status of the account .

protected void Page_Load(object sender, EventArgs e)
{
    try
    {
        if (Page.Request != null)
        {
            if (Page.Request.QueryString["UserName"] != null)
            {
                string UserName = Page.Request.QueryString["UserName"].ToString();
 
                if (HttpContext.Current.Session != null)
                {
                    uint LCID = (uint)HttpContext.Current.Session.LCID;
 
                    string StatusValue = String.Empty;
 
                    DirectoryEntry entry = null;
                    using (HostingEnvironment.Impersonate())
                    {
                        entry = new DirectoryEntry("LDAP://MyDomain/CN=" + UserName +",DC=MyDomain,DC=local");
 
                        if (entry != null)
                        {
                            long pwdLastSet = 2;
                            long ldate  = 0;
 
                            if (entry.Properties["pwdLastSet"].Value != null)
                            {
                                pwdLastSet = LongFromLargeIntegerObject(entry.Properties["pwdLastSet"].Value);
                            }
                            
                            if (entry.Properties["LockOutTime"].Value != null)
                            {
                                ldate = LongFromLargeIntegerObject(entry.Properties["LockOutTime"].Value);
                            }
                            
 
                            double? TimeRemainingUntilPasswordExpiration = GetTimeRemainingUntilPasswordExpiration(entry);
 
 
                            //Account Locked
                            if (ldate != 0)
                            {
                                StatusValue = GetLocalizedString("Status_LockedAccount", LCID) + DateTime.FromFileTime(ldate).ToString("dd/MM/yyyy");
                            }
                            //Account Password Expired
                            else if (pwdLastSet == 0)
                            {
                                StatusValue = GetLocalizedString("Status_PasswordExpired", LCID);
                            }
                            else if (TimeRemainingUntilPasswordExpiration != null && TimeRemainingUntilPasswordExpiration < 0)
                            {
                                StatusValue = GetLocalizedString("Status_PasswordExpired", LCID);
                            }
                            //Account Expired
                            else if (entry.ExpirationDate > new DateTime(2000, 1, 1, 2, 0, 0) && entry.ExpirationDate <= DateTime.Now)
                            {
                                StatusValue = GetLocalizedString("Status_AccountExpired", LCID);
                            }
                            else
                                StatusValue = GetStatusValue(LCID, StatusValue, entry);
 
                            SendNotificationMail(entry, LCID, StatusValue);
                            //lblStatus.Text = StatusValue;
                        }
                        else
                        {
                            lblStatus.Text = "Could not Query AD LDAP Operation , Entity object is NULL";
                        }
                    }        
                }
                else
                {
                    Response.Write("<!-- session null -->");
                }
            }
            else
            {
                lblStatus.Text = "No Query String supplied";
            }
        }
        else
        {
            lblStatus.Text = "Request rejected";
 
        }
    }
    catch (Exception ex)
    {
        lblStatus.Text = ex.Message + " " + ex.StackTrace;
    }
}
Following are the helper methods used in the page load method :


private static double? GetTimeRemainingUntilPasswordExpiration(DirectoryEntry entry)
{
    if (entry.Properties.Contains("maxPwdAge"))
    {
        long pwdLastSet = LongFromLargeIntegerObject(entry.Properties["pwdLastSet"].Value);
 
        if (entry.Properties["maxPwdAge"].Value != null && pwdLastSet != 0)
        {
            var maxPasswordAge = int.Parse(entry.Properties["maxPwdAge"].Value.ToString());
 
            return maxPasswordAge - (DateTime.Now - DateTime.FromFileTime(pwdLastSet)).TotalDays;
        }
        else
            return null;
    }
    else
        return null;
}
 
/// <summary>
///  Used for the date time of the locked out account
/// </summary>
/// <param name="largeInteger"></param>
/// <returns></returns>
private static long LongFromLargeIntegerObject(object largeInteger)
{
    System.Type type = largeInteger.GetType();
    int highPart = (int)type.InvokeMember("HighPart", BindingFlags.GetProperty, null,
  largeInteger, null);
    int lowPart = (int)type.InvokeMember("LowPart", BindingFlags.GetProperty, null, largeInteger, null);
    return (long)highPart << 32 | (uint)lowPart;
} 
Now if all those cases are not met we need to check the user account control property to get the current status of the account .

private static string GetStatusValue(uint LCID, string StatusValue, DirectoryEntry entry)
{
    string AccountControlValue = entry.Properties["userAccountControl"].Value.ToString();
 
    switch (AccountControlValue)
    {
        case "512":
            StatusValue = "Status_EnabledAccount";
            break;
        case "514":
            StatusValue = "Status_DisabledAccount";
            break;
        case "544":
            StatusValue = "Status_EnabledPasswordNotRequired";
            break;
        case "546":
            StatusValue = "Status_DisabledPasswordNotRequired";
            break;
        case "66048":
            StatusValue = "Status_EnabledPasswordDoesnotExpire";
            break;
        case "66050":
            StatusValue = "Status_DisabledPasswordDoesnotExpire";
            break;
        case "66080":
            StatusValue = "Status_EnabledPasswordDoesnotExpireNotRequired";
            break;
        case "66082":
            StatusValue = "Status_DisabledPasswordDoesnotExpireNotRequired";
            break;
        case "262656":
            StatusValue = "Status_EnabledSmartcardRequired";
            break;
        case "262658":
            StatusValue = "Status_DisabledSmartcardRequired";
            break;
        case "262688":
            StatusValue = "Status_EnabledSmartcardRequiredPasswordNotRequired";
            break;
        case "262690":
            StatusValue = "Status_DisabledSmartcardRequiredPasswordNotRequired";
            break;
        case "328192":
            StatusValue = "Status_EnabledSmartcardRequiredPasswordDoesnotExpire";
            break;
        case "328194":
            StatusValue = "Status_DisabledSmartcardRequiredPasswordDoesnotExpire";
            break;
        case "328224":
            StatusValue = "Status_EnabledSmartcardRequiredPasswordDoesnotExpireNotRequired";
            break;
        case "328226":
            StatusValue = "Status_DisabledSmartcardRequiredPasswordDoesnotExpireNotRequired";
            break;
        default:
            StatusValue = "Status_NotAvailable";
            break;
    }
    return StatusValue ;
}
 
 
Hope you find this post useful :)