July 16, 2009

A better Ldap Error Message

Yesterday, I showed a simple way to do Ldap login using C# for your .NET application. To expand on that a drop, I wanted to show how exactly you use that class with a custom formatted and cleaned up Error Message.

First, here is how you use the class from yesterday (pseudocode):
string user = //passed in from somewhere;
string password = //passed in from somewhere;
string domain = //get from config or passed in from somewhere;

try {
// Note: The server is not really needed in our original class...I added it just in case
LdapLogin.VerifyCredentials(adSettings.Server, user, domain, password);

//return success
}
catch {LdapException ex) {
LdapErrorMessage error = new LdapErrorMessage(ex);
//return string.Format("Error. {0} {1} {2}", ex.Message, error.Description, ex.ServerErrorMessage)
Let's focus on the LdapErrorMessage part in the catch. The typical LdapException returns things that may not be exactly what you want to see or even what the error really is. Reading the hex code that is returned back, you can actually get a better error message like "User not found" or "Not permitted to logon at this time.". Here is the class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices.Protocols;
using System.Text.RegularExpressions;
using System.Globalization;

namespace JLFramework
{
public class LdapErrorMessage
{
public string Message { get; set; }
public int Code { get; set; }
public string LdapErr { get; set; }
public string Comment { get; set; }
public int Data { get; set; }
public string Version { get; set; }

/// 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 525, v893
/// 80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 0, vece
public LdapErrorMessage(LdapException ex)
{
if (string.IsNullOrEmpty(ex.ServerErrorMessage))
return;

this.Message = ex.ServerErrorMessage;

string pattern = @"(.*): LdapErr: (.*), comment: (.*), data (.*), (.*)";
Match match = Regex.Match(ex.ServerErrorMessage, pattern, RegexOptions.IgnoreCase);

if (match.Success)
{
this.Code = ParseHex(match.Groups[1].Value);
this.LdapErr = match.Groups[2].Value;
this.Comment = match.Groups[3].Value;
this.Data = ParseHex(match.Groups[4].Value);
this.Version = match.Groups[5].Value;
}
}

public string Description
{
get
{
// http://wiki.caballe.cat/index.php/Active_Directory_LDAP_Errros
switch (this.Data)
{
case 0x525: return "User not found.";
case 0x52e: return "Invalid credentials.";
case 0x530: return "Not permitted to logon at this time.";
case 0x532: return "Password expired.";
case 0x533: return "Account disabled.";
case 0x701: return "Account expired.";
case 0x773: return "User must reset password.";

default:
return "";
}
}
}

private int ParseHex(string s)
{
int n;
if (int.TryParse(s, NumberStyles.HexNumber, null, out n))
return n;
else
return 0;
}
}
}

One note though from a security stand-point you actually probably don't want to do this for external or perhaps even internal sites. You never want to let users know the actual error since hackers can deduce from there a way to get in. For example by saying "Account expired", I know that that the user and perhaps even password is correct on a brute force attack. Instead, I should give a catch all exception of "UserName/Password combination failed". However, from a testing perspective this is always helpful.

No comments:

Post a Comment