skip to Main Content

I have this Action method in ASP.NET MVC 5:

namespace LDAPMVCProject.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult UsersInfo(string username, string password)
        {
            DomainContext result = new DomainContext();

            try
            {
                // create LDAP connection object  
                DirectoryEntry myLdapConnection = createDirectoryEntry();
                string ADServerName = System.Web.Configuration.WebConfigurationManager.AppSettings["ADServerName"];
                string ADusername = System.Web.Configuration.WebConfigurationManager.AppSettings["ADUserName"];
                string ADpassword = System.Web.Configuration.WebConfigurationManager.AppSettings["ADPassword"];

                using (var context = new DirectoryEntry("LDAP://mydomain.com:389/DC=mydomain,DC=com", ADusername, ADpassword))
                using (var search = new DirectorySearcher(context))
                {
                    // validate username & password
                    using (var context2 = new PrincipalContext(ContextType.Domain, "mydomain.com", ADusername, ADpassword))
                    {
                        bool isvalid = context2.ValidateCredentials(username, password);
                        
                        if !(isvalid)
                            return **** // authentication error
                    }

                    // create search object which operates on LDAP connection object  
                    // and set search object to only find the user specified  

                    //    DirectorySearcher search = new DirectorySearcher(myLdapConnection);
                    //  search.PropertiesToLoad.Add("telephoneNumber");
                    search.Filter = "(&(objectClass=user)(sAMAccountName=test.test))";

                    // create results objects from search object  

                    // user exists, cycle through LDAP fields (cn, telephonenumber etc.)  
                    SearchResult r = search.FindOne();

                    ResultPropertyCollection fields = r.Properties;

                    foreach (String ldapField in fields.PropertyNames)
                    {
                        if (ldapField.ToLower() == "telephonenumber")
                        {
                            foreach (Object myCollection in fields[ldapField])
                            {
                                result.Telephone = myCollection.ToString();
                            }
                        }
                        else if (ldapField.ToLower() == "department")
                        {
                            foreach (Object myCollection in fields[ldapField])
                            {
                                result.Department = myCollection.ToString();
                            }
                        }
                       // }
                    }
                    
                    if (result.Telephone == null)
                        return ***** //Telephone is empty

                    if (result.Department)
                        return **** // department is empty

                    string output = JsonConvert.SerializeObject(result);

                    return Content(output, "application/json");//success
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception caught:nn" + e.ToString());
            }

            return View(result);
        }
    }
}

The action method acts as an API endpoint for our web application, where the API accepts username & password, and does the following:

  1. Validate the username/password against Active Directory

  2. If valid; check if the telephone number is empty >> if so return an error

  3. If valid; check if department is empty >> if so return an error

  4. If valid and info found; return the department & telephone for the user

Now I am a bit confused on how I need to return the JSON for the first 3 points? Should I always return http 200 with a status message (Status : "success" OR Status: "failed")? or if the username/password validation failed then i should return http 401 without having to return any JSON content?

Can anyone help me with this?

I need to write the action method in a standard way that can be consumed by 3rd party application.

Second question: what do I need to return in case the code raised an exception?

Thanks

3

Answers


  1. There are a lot of ways to go about this and ultimately you want to have your endpoint behave in a way that whoever is consuming your endpoint expects.

    I stumbled across this as an interesting way to handle nuanced errors in a request to your endpoint. Even though this is used for Graph API, you could use the concept for your needs. https://developers.facebook.com/docs/graph-api/guides/error-handling. The TL;DR is to have a standardized json response like:

    {
      "error": {
        "message": "Message describing the error", 
        "type": "OAuthException", 
        "code": 190,
        "error_subcode": 460,
        "error_user_title": "A title",
        "error_user_msg": "A message",
        "fbtrace_id": "EJplcsCHuLu"
      }
    }
    
    Login or Signup to reply.
  2. The HTTP statuse codes are very flexable and can be confused to tell when to use what.
    My advice:

    1. Identify the Http status family (X00)
    • 100s: Informational codes: the server acknowledges the request
      initiated by the browser and that it is being processed (100–199).
    • 200s: Success codes: request received, understood, processed and
      expected info relayed to browser (200–299).
    • 300s: Redirection codes: a different destination has been substituted
      for the requested resource; further action by the browser may be
      required (300–399).
    • 400s: Client error codes: website or page not reached; page
      unavailable or there was a technical problem with the request
      (400–499).
    • 500s: Server error codes
    1. Search for the specific Http status code for your response (2XX) here some exemples for the 200 family:
    • 201: Created. Request fulfilled; new resource created. Typical response
      after POST requests.
    • 202: Accepted. Browser request accepted, still in process. May or may not
      succeed.

    For your example I would return:

    • 403: Forbidden – if the user credentials are wrong.
    • 200: Ok – if everythig works well (all the info returned).

    The other option is a little tricky, when the user is authenticate but have no valid data.
    you can return:

    • 204: No content – because the user is auth but has no data
    • 500: internal server error – because the server cant return the requested
      object
    • 404: Not found – (not my personal chois but it is an option)

    It also depends on your client and you preferences.

    Happy coddind 🙂

    Login or Signup to reply.
  3. This is an API error handling and logging design, and the following type of approach works well, to separate the concerns and keep your main logic clean:

    DESIGN ERROR RESPONSES

    These should be useful to clients, eg if they need to display an error or do something based on a specific cause. A 4xx error might have this payload, along with an HTTP status:

    {
      "code": "authentication_failed",
      "message": "Invalid credentials were provided"
    }
    

    A 500 error is often given a different payload based on what a UI will display in this case, and how you look the error up in logs:

    {
      "code": "authentication_error",
      "message": "A problem was encountered during a backend authentication operation",
      "area": "LDAP",
      "id": 12745,
      "utcTime": "2022-07-24T10:27:33.468Z"
    }
    

    DESIGN API LOGS

    In the first case the server logs might have fields such as these:

    {
      "id": "7af62b06-8c04-41b0-c428-de332436d52a",
      "utcTime": "2022-07-24T10:27:33.468Z",
      "apiName": "MyApi",
      "operationName": "getUserInfo",
      "hostName": "server101",
      "method": "POST",
      "path": "/userInfo",
      "errorData": {
        "statusCode": 401,
        "clientError": {
          "code": "authentication_failed",
          "message": "Invalid credentials were provided",
          "context": "The account is locked out"
        }
      }
    }
    

    In the second case the server logs might have fields such as these:

    {
      "id": "7af62b06-8c04-41b0-c428-de332436d52a",
      "utcTime": "2022-07-24T10:27:33.468Z",
      "apiName": "MyApi",
      "operationName": "getUserInfo",
      "hostName": "server101",
      "method": "POST",
      "path": "/userInfo",
      "errorData": {
        "statusCode": 500,
        "clientError": {
          "code": "authentication_error",
          "message": "A problem was encountered during a backend authentication operation",
          "area": "LDAP",
          "id": 12745,
          "utcTime": "2022-07-24T10:27:33.468Z"
        },
        "serviceError": {
          "details": "Host not found: error MS78245",
          "stack": [
            "Error: An unexpected exception occurred in the API",
            "at DirectorySearcher: 871 ... "
          ]
      }
    }
    

    CODE

    Perhaps aim to use code similar to this, to represent your desired error and logging behaviour. The ClientError and ServiceError classes enable the above responses and logs. When errors are thrown this should enable you to add useful contextual info:

    public class HomeController : Controller
    {
        public ActionResult UsersInfo(string username, string password)
        {
            DomainContext result = new DomainContext();
    
            try
            {
                DirectoryEntry myLdapConnection = createDirectoryEntry();
                string ADServerName = System.Web.Configuration.WebConfigurationManager.AppSettings["ADServerName"];
                string ADusername = System.Web.Configuration.WebConfigurationManager.AppSettings["ADUserName"];
                string ADpassword = System.Web.Configuration.WebConfigurationManager.AppSettings["ADPassword"];
    
                using (var context = new DirectoryEntry("LDAP://mydomain.com:389/DC=mydomain,DC=com", ADusername, ADpassword))
                using (var search = new DirectorySearcher(context))
                {
                    using (var context2 = new PrincipalContext(ContextType.Domain, "mydomain.com", ADusername, ADpassword))
                    {
                        bool isvalid = context2.ValidateCredentials(username, password);
                        if !(isvalid)
                            throw new ClientError(401, "authentication_failed", "Invalid credentials were provided", "optional context goes here");
                    }
    
                    DirectorySearcher search = new DirectorySearcher(myLdapConnection);
                    search.Filter = "(&(objectClass=user)(sAMAccountName=test.test))";
    
                    SearchResult r = search.FindOne();
                    ResultPropertyCollection fields = r.Properties;
                    foreach (String ldapField in fields.PropertyNames)
                    {
                        if (ldapField.ToLower() == "telephonenumber")
                        {
                            foreach (Object myCollection in fields[ldapField])
                            {
                                result.Telephone = myCollection.ToString();
                            }
                        }
                        else if (ldapField.ToLower() == "department")
                        {
                            foreach (Object myCollection in fields[ldapField])
                            {
                                result.Department = myCollection.ToString();
                            }
                        }
                    }
                    
                    if (result.Telephone == null)
                        throw new ClientError(400, "invalid_user_data", "User data is invalid", "Telephone is missing");
    
                    if (result.Department)
                        throw new ClientError(400, "invalid_user_data", "User data is invalid", "Department is missing");
    
                    string output = JsonConvert.SerializeObject(result);
                    return Content(output, "application/json");
                }
            }
            catch (Exception e)
            {
                throw new ServiceError("authentication_error", "A problem was encountered during a backend authentication operation", "LDAP", e);
            }
    
            return View(result);
        }
    }
    

    MIDDLEWARE

    The usual pattern is then to use small middleware classes to deal with processing exceptions, returning error responses and writing error logs:

    The type of logic written here will depend a little on your preferences, but might look similar to this:

    public class ErrorFilterAttribute : HandleErrorAttribute
    {
        public override void OnException(ExceptionContext filterContext)
        {
            var logEntry = new ErrorLogEntry();
            var jsonResponse = ""
            var statusCode = 500;
    
            if (filterContext.Exception is ClientError)
            {
                var clientError = filterContext.Exception as ClientError;
                logEntry.AddClientErrorDetails(clientError);
                statusCode = clientError.StatusCode;
                jsonResponse = clientError.toResponseFormat();
            }
            if (filterContext.Exception is ServiceError)
            {
                var serviceError = filterContext.Exception as ServiceError;
                logEntry.AddServiceErrorDetails(serviceError);
                statusCode = serviceError.StatusCode;
                jsonResponse = serviceError.toResponseFormat();
            }
            logEntry.Write();
    
            filterContext.Result = new JsonResult(jsonResponse);
            filterContext.HttpContext.Response.Clear();
            filterContext.HttpContext.Response.StatusCode = statusCode;
            filterContext.ExceptionHandled = true;
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search