Friday, February 8, 2013

Using Active Directory authentication in MVC4

We bought the new book Pro ASP.NET MVC4 by Adam Freeman, which is a great book and very well written.  However, in the authentication section, he assumes (probably rightly) most folks will be writing applications for the broader internet and want to implement a stand-alone means of authentication (probably wrongly).  It seems to me that these days you deal your site out unless people can single sign-on in some form or fashion using an existing google or facebook account or other account.  Anyhow, for our Project Tracker application, we're all in-house and Adam didn't have any explanation for setting up AD Authentication, so this is how it is done.

First, you'll need to find the LDAP url you'll use to communicate with the windows domain server that hosts your Active Directory accounts.  If you don't know it, open a command prompt and type:

gpresult /r

That'll give you a bunch of information related to your account and will include a line like:

USER SETTINGS
--------------
    CN=Ken Burcham,OU=Users,OU=Geographic Information Systems,OU=Office of Infor
mation Technology,OU=Administration,OU=CTUIR,DC=mailcomm,DC=ctuir,DC=com
    Last time Group Policy was applied: 2/7/2013 at 1:19:28 PM
    Group Policy was applied from:      CTUIR-DCX.mailcomm.ctuir.com
    Domain Name:                        MAILCOMM


From this, you'll need to construct an LDAP connection string and add it to the connectionStrings section of  your web.config.  For me, was something like this:



<add name="ADConnectionString" connectionString ="LDAP://CTUIR-DCX.mailcomm.ctuir.com:389/DC=mailcomm,DC=ctuir,DC=com" />


Note that the default port is 389 but if you are connecting from external to your network you'll certainly want to connect using SSL which for LDAP is port 636. Also note that the order of the elements is important. :) You'll also need to add the following section to your web.config to enable the ADMembershipProvider:

<membership defaultProvider="ADMembershipProvider">
      <providers>
        <clear/>
        <add name="ADMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider" connectionStringName="ADConnectionString" attributeMapUsername="sAMAccountName"/>
      </providers>
    </membership>

Next, configure your application to handle authentication by adding this to your Global.asax.cs file:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new System.Web.Mvc.AuthorizeAttribute());
        }

Now you need to connect the hoses in your AccountController. This section is mostly following the instructions posted by Ricardo Sanchez here: How to secure your asp net mvc application

[Authorize]
    public class AccountController : Controller
    {
        //
        // GET: /Account/Login

        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        //
        // POST: /Account/Login

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                if(Membership.ValidateUser(model.UserName, model.Password))
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    if(Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index","Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("","The user name or password provided is incorrect.");
                }
            }
            
            return View(model);
        }

        //
        // POST: /Account/LogOff

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            FormsAuthentication.SignOut();

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

That's about it. One last thing, I used this at the top of my _loginPartial.cshtml which worked nicely to display the currently logged in user:

@(User.Identity.IsAuthenticated ? "Hello, " + User.Identity.Name : "Not logged in.")

Well, I didn't find all of this in one place, so hopefully it'll help someone else! :)

Wednesday, February 6, 2013

Technical information for ExchangeNetwork and WQX

When working on my WQXUploader utility, the STORET and ExchangeNetwork helpdesk provided me a number of links that were helpful, as well as some I found on my own.  Here is a collection of technical information that was helpful for reference:

The top level of the ExchangeNetwork information:
 
 
Two open source projects that were great for code browsing:
This was a helpful example in getting my Visual Studio project started:
 
These were great for details:
 

Tuesday, February 5, 2013

Uploading multiple water temperature files to CDX/WQX Exchange Network

Upload tool for submitting multiple files at once to the WQX

We have more than 100 xml files of water temperature to upload to the WQX water quality exchange, which is an Axis endpoint located here:

Water Quality Exchange (WQX)

Node 2.1 Test – https://testngn.epacdxnode.net/ngn-enws20/services/NetworkNode2ServiceConditionalMTOM
Node 2.1 Production – https://cdxnodengn.epa.gov/ngn-enws20/services/NetworkNode2ServiceConditionalMTOM
Node 1.1 Test – https://testngn.epacdxnode.net/cdx-enws10/services/NetworkNodePortType_V10
Node 1.1 Production – https://cdxnodengn.epa.gov/cdx-enws10/services/NetworkNodePortType_V10

Getting the account(s) setup

WQXWeb account

First I had to get my WQX Web account setup.  I contacted the STORET folks who setup my account by following these instructions: http://www.epa.gov/storet/wqx/wqxweb.html.  I emailed their helpdesk with the information they asked for and they setup my account. Then I could login to http://cdx.epa.gov and view our organization's data as well as manually upload individual files.

Rather than upload the files individually through the web interface, I wanted to upload the files in bulk.  On the getting started with WQX page (http://www.epa.gov/storet/wqx/wqx.html) they suggest using the Windsor Node Client Lite 2 software (http://www.windsorsolutions.biz/nodeclient/disclaimer.html) so I downloaded it and followed the instructions on the getting started page for getting the endpoints configured.  But I ran into a problem.

NAAS Account

As it turns out, the username/password that STORET gives you is not a NAAS account.  You need to have a NAAS account in order to submit data using the endpoints above, which includes submitting files using the Node Client Lite 2 software.

To setup a NAAS account, I needed to email nodehelpdesk@epacdx.net and request a NAAS account that is linked to my STORET account and provide them with my STORET account id and organization id.  They contacted the STORET helpdesk and asked for their approval to set me up.  Once I was approved, they sent me my new, shiny NAAS account.

Using the new NAAS credentials to login with the Node Client Lite 2 software, I was able to submit a file to the test node.  Once that was successful, I needed to contact the nodehelpdesk@epacdx.net guys again and give them the successfully submitted transaction id so that they could verify that everything about my file was good and then they enabled me for submitting to the production node.

Now I thought I was all set!  I selected a bunch of files through the Node Client Lite user interface but received an error that the endpoint expected one file but received many.  I asked the helpdesk about it and they informed me that I could only send one file at a time.  Bummer!  So I decided to write my own client that would upload my files.  We want to automate this whole process in the future anyway, so I thought I'd just whip up a connector.

Writing WQXUpload tool

These days, I'm writing in c# using Visual Studio 2012.  So I fired it up and created a new project.  Then I went to PROJECT -> add service reference, clicked "Advanced" button and then "Add Web Reference".  I pasted the URL for the endpoint I'm using (https://testngn.epacdxnode.net/cdx-enws10/services/NetworkNodePortType_V10 I'd definitely start with the test url) and then let it build my SOAP connector class automagically.

Then here is my code for the uploader.  Enjoy!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConsoleApplication1.gov.epa.cdxnodengn;
using ConsoleApplication1.gov.epa.cdx;
using System.IO;

namespace ConsoleApplication1
{
    class WQXUploader
    {
        private static string defaultFilePath = "."; //all .xml files in this directory will be uploaded.

        private static NetworkNode node;
        private static string token;
        private static int numFilesUploaded = 0;


        static int Main(string[] args)
        {

            if (args.Length < 2 )
            {
                log("Syntax: WQXUpload   [path] (default path is current directory)");
                waitForKeypress();
                return 1;
            }

            //setup our passed in credentials.
            string username = args[0];
            string password = args[1];

            //if they call us with a path then set it here.
            string filePath = defaultFilePath;
            if (args.Length == 3)
            {
                filePath = args[2];
            }

            //done with args now get rolling...
            log("Starting WQX Uploader.");
            Inititialize();
            Authenticate(username, password);

            //which files to upload?
            List filesToUpload = getFilesToUpload(filePath);

            //submit each file.
            foreach (var file in filesToUpload)
            {
                //log("Uploading file: " + file);
                submitFile(token, file, filePath);
                numFilesUploaded++;
            }

            Cleanup();
            log("WQX upload is finished. We uploaded "+ numFilesUploaded + " files." );
            waitForKeypress();

            return 0;
        }

        /**
         * Gets a list of files from the filepath that we will upload
         * @return List files to upload
         */ 
        private static List getFilesToUpload(string filePath)
        {
            log("Using path: " + filePath);

            List files = new List();

            DirectoryInfo di = new DirectoryInfo(filePath);
            var directories = di.GetFiles("*.xml", SearchOption.TopDirectoryOnly);

            foreach (FileInfo file in directories)
            {
                files.Add(file.Name);
            }

            return files;
        }

        /*
         * Wait for a keypress -- just to keep the console open.
         */ 
        private static void waitForKeypress()
        {
            Console.WriteLine("Press ESC to exit");
            do
            {
                while (!Console.KeyAvailable)
                {
                    // Do something
                }
            } while (Console.ReadKey(true).Key != ConsoleKey.Escape);
        }

        /**
         * Submits file to WQX
         * @return transactionId of the submission.
         */ 
        private static string submitFile(string token, string fileToUpload, string filePath)
        {
            NodeDocument doc = new NodeDocument();

            byte[] buffer = File.ReadAllBytes(filePath + "\\" + fileToUpload);

            doc.name = fileToUpload;
            doc.type = "xml";
            doc.content = buffer; 

            NodeDocument[] docToSend = { doc };

            String transactionId = node.Submit(token, "", "WQX", docToSend);

            log(fileToUpload + " " + transactionId);
            //logStatus(transactionId);

            return transactionId;

        }

        /*
         * Outputs the status of the transaction to the log.
         */ 
        private static void logStatus(string transactionId)
        {
            log(" Current status: " + node.GetStatus(token, transactionId));
        }

        /*
         * Writes the message to the log
         */ 
        private static void log(string msg)
        {
            Console.WriteLine(msg);
        }

        /*
         * Initializes the NetworkNode connection
         */ 
        private static void Inititialize()
        {
            node = new NetworkNode();
        }

        /**
         * Performs any cleanup necessary
         */ 
        private static void Cleanup()
        {
            node.Dispose();
        }

        /*
         * Connects and authenticates us with the endpoint, fetching our token necessary for other functions.
         * @return string token
         */ 
        private static string Authenticate(String username, String password)
        {
            log("Connecting and authenticating...");
            token = node.Authenticate(username, password, "password");
            log("Success! Security token: " + token);
            return token;
        }
    }
}