Wednesday, August 5, 2009

ServicePrincipalNameCheck.cs – Check your SPN

In case you are programming with Windows Communication Foundation (WCF), you might want to use Kerberos authentication (instead of NTLM) one day. An example of an WCF client and server that use this security mode can be found on MSDN.

Kerberos authentication is faster than NTLM and more secure. The bad thing is, in order Kerberos is able to authenticate, it will need a ticket and for that it needs a service principal name (SPN) inside Active Directory.

I don’t want to explain all the details about SPNs, since other people already have done this already: 3 Simple Rules to Kerberos Authentication/Delegation SPNs and Using WCF service in load balanced environment with Kerberos delegation.

However, since I just finished the third projects were missing SPNs have caused a, well, not-as-planned, project start, I have created a class that can check SPNs.

The idea behind is quite simple: When the server starts, it will use this class to check if all necessary SPNs are set. If not, the class will return FALSE and you can use the Log property to log an error. That way, the admin is aware that there is a problem.

Please note that this class is intent for self-hosting services. This means, it will check if a SPN of SERVICENAME/HOSTNAME:PORT exists, e.g. CalcService/SERVER01:45563. If you plan to use it from IIS hosted services as well, you need to change this.

Anyway, here we go:

using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Net;

namespace PleaseChangeMeIDoNotLikeMe

{
    /*
     * This class is intent to check the SPN (Service principal Names) inside a domain environment.
     *
     * You need a SPN as soon as your client wants to use Kerberos authentication, for example: http://msdn.microsoft.com/en-us/library/ms735117.aspx
     *
     * Create this class with the service name you want to use and the port, e.g. “MyService” and 8081.  After that, call the function Check();
     *
     *
     * Given that the current computer name is “DaServer”, this class will search if a SPN of "MyService/DaServer" and
     * "MyService/DaServer.yourdnsdomain.ext" exists.
     *
     * If either of these two does not exist, Check will result FALSE. If more than one exists, it will also return FALSE.
     *
     * Please note: SPN are NOT case sensitive when used in Windows only environments, but are case sensitive when using with UNIX clients.
     *
     *
     * More information about SPN can be found here: http://technet.microsoft.com/en-us/library/cc731241%28WS.10%29.aspx
     *    
     */

    //Copyright (C) 2009 Xteq Systems - http://www.xteq.com/
    //Copyright (C) 2009 TeX Hex - http://www.texhex.info - http://texhex.blogspot.com/
    public sealed class ServicePrincipalNameCheck
    {
        string _service = string.Empty;
        int _port = 0;

        StringBuilder _log = new StringBuilder();

        StringCollection _adsPaths = new StringCollection();
        StringCollection _spns = new StringCollection();

        public ServicePrincipalNameCheck(string ServiceName, int Port)
        {
            if (string.IsNullOrEmpty(ServiceName))
            {
                throw new ArgumentException("ServiceName must be filled");
            }
            _service = ServiceName;

            if (Port <= 0)
            {
                throw new ArgumentException("Port must not be zero");
            }
            _port = Port;
        }

        public string Log
        {
            get
            {
                return _log.ToString();
            }
        }

        public bool Check()
        {
            _log = new StringBuilder();
            _adsPaths.Clear();
            _spns.Clear();

            string computerName = System.Environment.MachineName;
            string fqdn = Dns.GetHostEntry(computerName).HostName;

            _log.AppendLine(string.Format("Computer NetBIOS name: {0}", computerName));
            _log.AppendLine(string.Format("Computer FQDN: {0}", fqdn));
            _log.AppendLine();

            bool bCheckResult1=CheckInternal(computerName);
            bool bCheckResult2 = CheckInternal(fqdn);

            bool bCheckHostCount = true;

            //Write out found AD objects
            _log.AppendLine("Found computer(s):");
            if (_adsPaths.Count > 0)
            {
                foreach (string sHost in _adsPaths)
                {
                    _log.AppendLine(sHost);
                }
            }
            else
            {
                _log.AppendLine("(none)");
            }
            _log.AppendLine();

            //Check if we have more than one entry in _adsPath. If so, several hosts have registered our SPN
            //which is normally not that good idea
            if (_adsPaths.Count > 1)
            {
                _log.AppendLine("Error: More than one AD host was found!");
                bCheckHostCount = false;
            }

            //Write out found SPN
            _log.AppendLine("Found SPN(s) (might include other SPNs as well)");
            if (_spns.Count > 0)
            {
                foreach (string sSPN in _spns)
                {
                    _log.AppendLine(sSPN);
                }
            }
            else
            {
                _log.AppendLine("(none)");
            }
            return bCheckResult1 && bCheckResult2 && bCheckHostCount;
        }

        private bool CheckInternal(string Computer)
        {
            string serviceName = string.Format("{0}/{1}:{2}", _service, Computer, _port);

            _log.AppendLine("Checking for SPN: " + serviceName);

            bool bResult = false;

            string filter = string.Format("(serviceprincipalname={0})", serviceName);

            using (Domain localDomain = Domain.GetCurrentDomain())
            {
                using (DirectorySearcher search = new DirectorySearcher(localDomain.GetDirectoryEntry()))
                {
                    search.Filter = filter;
                    search.SearchScope = SearchScope.Subtree;

                    SearchResultCollection resultColl = search.FindAll();

                    if (resultColl.Count <= 0)
                    {
                        _log.AppendLine("Error: No SPN found!");
                        bResult = false;
                    }
                    else
                    {
                        if (resultColl.Count > 1)
                        {
                            _log.AppendLine("Error: More than one matching SPN found!");
                            bResult = false;
                        }
                        else
                        {
                            _log.AppendLine("SPN check okay");
                            bResult = true;
                        }

                        ProcessResults(resultColl);
                    }

                    _log.AppendLine();
                    return bResult;                   
                }
            }

        }

        void ProcessResults(SearchResultCollection resultColl)
        {
            ResultPropertyValueCollection rpvc;

            foreach (SearchResult res in resultColl)
            {
                rpvc = res.Properties["adsPath"];
                SaveResults(rpvc, _adsPaths);

                rpvc = res.Properties["serviceprincipalname"];
                SaveResults(rpvc, _spns);
            }

        }

        private void SaveResults(ResultPropertyValueCollection rpvc, StringCollection sColl)
        {
            foreach (Object propValue in rpvc)
            {
                string s=propValue.ToString();
                if (sColl.Contains(s) == false)
                {
                    sColl.Add(s);
                }
            }
        }

    }
}

No comments:

Post a Comment