Create an FTP Authentication Provider using an XML Database

by Robert McMurray

Microsoft has created a new FTP service that has been completely rewritten for Windows ServerĀ® 2008. This new FTP service incorporates many new features that enable Web authors to publish content more easily than before, and offers Web administrators more security and deployment options. The new FTP 7.5 service supports extensibility that lets you extend the built-in functionality that is included with the FTP service. More specifically, FTP 7.5 supports the creation of your own authentication, home directory, and logging providers.

This walkthrough will lead you through the steps to use managed code to an FTP authentication provider that uses the sample XML file from the following walkthrough for users and roles:

Prerequisites

The following items are required to complete the procedures in this article:

  1. IIS 7.0 or above must be installed on your Windows Server 2008 server, and the Internet Information Services (IIS) Manager must also be installed.

  2. The new FTP 7.5 service must be installed. You can download and install the FTP 7.5 service from the https://www.iis.net/ web site using one of the following links:

  3. You must have FTP publishing enabled for a site.

  4. You must use Visual Studio 2008.

    Note

    If you use an earlier version of Visual Studio, some of the steps in this walkthrough may not be correct.

Important

To help improve the performance for authentication requests, the FTP service caches the credentials for successful logins for 15 minutes by default. This means that if you change the password in your XML file, this change may not be reflected for the cache duration. To alleviate this, you can disable credential caching for the FTP service. To do so, use the following steps:

  1. Open a command prompt.

  2. Type the following commands:

    cd /d "%SystemRoot%\System32\Inetsrv"
    Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
    Net stop FTPSVC
    Net start FTPSVC
    
  3. Close the command prompt.

Step 1: Set up the Project Environment

In this step, you will create a project in Visual Studio 2008 for the demo provider.

  1. Open Microsoft Visual Studio 2008.

  2. Click the File menu, then New, then Project.

  3. In the New Project dialog box:

    • Choose Visual C# as the project type.
    • Choose Class Library as the template.
    • Type FtpXmlAuthentication as the name of the project.
    • Click OK.
  4. When the project opens, add a reference path to the FTP extensibility library:

    • Click Project, and then click FtpXmlAuthentication Properties.

    • Click the Reference Paths tab.

    • Enter the path to the FTP extensibility assembly for your version of Windows, where C: is your operating system drive.

      -For Windows Server 2008 and Windows Vista: C:\Windows\assembly\GAC\_MSIL\Microsoft.Web.FtpServer\7.5.0.0\_\_31bf3856ad364e35 -For Windows 7: C:\Program Files\Reference Assemblies\Microsoft\IIS

    • Click Add Folder.

  5. Add a strong name key to the project:

    • Click Project, and then click FtpXmlAuthentication Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpXmlAuthenticationKey for the key file name.
    • If desired, enter a password for the key file; otherwise, clear the Protect my key file with a password check box.
    • Click OK.
  6. Optional: You can add a custom build event to add the DLL automatically to the Global Assembly Cache (GAC) on your development computer:

    • Click Project, and then click FtpXmlAuthentication Properties.

    • Click the Build Events tab.

    • Enter the following in the Post-build event command line dialog box:

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. Save the project.

Step 2: Create the Extensibility Class

In this step, you will implement the logging extensibility interface for the demo provider.

  1. Add a reference to the FTP extensibility library for the project:

    • Click Project, and then click Add Reference.
    • On the .NET tab, click Microsoft.Web.FtpServer.
    • Click OK.
  2. Add a reference to System.Web for the project:

    • Click Project, and then click Add Reference.
    • On the .NET tab, click System.Web.
    • Click OK.
  3. Add a reference to System.Configuration for the project:

    • Click Project, and then click Add Reference.
    • On the .NET tab, click System.Configuration.
    • Click OK.
  4. Add the code for the authentication class:

    • In Solution Explorer, double-click the Class1.cs file.

    • Remove the existing code.

    • Paste the following code into the editor:

      using System;
      using System.Collections;
      using System.Collections.Specialized;
      using System.Collections.Generic;
      using System.Configuration.Provider;
      using System.IO;
      using System.Linq;
      using System.Text;
      using System.Xml;
      using Microsoft.Web.FtpServer;
      using System.Xml.XPath;
      
      // Define the XML authentication provider class.
      public class FtpXmlAuthentication :
        BaseProvider,
        IFtpAuthenticationProvider,
        IFtpRoleProvider
      {
        // Create a string to store the path to the XML file that stores the user data.
        private static string _xmlFileName;
      
        // Create a file system watcher object for change notifications.
        private static FileSystemWatcher _xmlFileWatch;
      
        // Create a dictionary to hold user data.
        private static Dictionary<string, XmlUserData> _XmlUserData =
          new Dictionary<string, XmlUserData>(
            StringComparer.InvariantCultureIgnoreCase);
      
        // Override the Initialize method to retrieve the configuration settings.
        protected override void Initialize(StringDictionary config)
        {
          // Retrieve the path to the XML file.
          _xmlFileName = config["xmlFileName"];
      
          // Test if the path is empty.
          if (string.IsNullOrEmpty(_xmlFileName))
          {
            // Throw an exception if the path is missing or empty.
            throw new ArgumentException("Missing xmlFileName value in configuration.");
          }
      
          // Test if the file exists.
          if (File.Exists(_xmlFileName) == false)
          {
            // Throw an exception if the file does not exist.
            throw new ArgumentException("The specified XML file does not exist.");
          }
      
          try
          {
            // Create a file system watcher object for the XML file.
            _xmlFileWatch = new FileSystemWatcher();
            // Specify the folder that contains the XML file to watch.
            _xmlFileWatch.Path = _xmlFileName.Substring(0, _xmlFileName.LastIndexOf(@"\"));
            // Filter events based on the XML file name.
            _xmlFileWatch.Filter = _xmlFileName.Substring(_xmlFileName.LastIndexOf(@"\") + 1);
            // Filter change notifications based on last write time and file size.
            _xmlFileWatch.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size;
            // Add the event handler.
            _xmlFileWatch.Changed += new FileSystemEventHandler(this.XmlFileChanged);
            // Enable change notification events.
            _xmlFileWatch.EnableRaisingEvents = true;
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
         }
      
        // Define the event handler for changes to the XML file.
        public void XmlFileChanged(object sender, FileSystemEventArgs e)
        {
          // Verify that the changed file is the XML data file.
          if (e.Name.Equals(
            _xmlFileName.Substring(_xmlFileName.LastIndexOf(@"\") + 1),
            StringComparison.OrdinalIgnoreCase))
          {
            // Clear the contents of the existing user dictionary.
            _XmlUserData.Clear();
            // Repopulate the user dictionary.
            ReadXmlDataStore();
          }
        }
      
        // Define the AuthenticateUser method.
        bool IFtpAuthenticationProvider.AuthenticateUser(
               string sessionId,
               string siteName,
               string userName,
               string userPassword,
               out string canonicalUserName)
        {
          // Define the canonical user name.
          canonicalUserName = userName;
      
          // Validate that the user name and password are not empty.
          if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userPassword))
          {
            // Return false (authentication failed) if either are empty.
            return false;
          }
          else
          {
            try
            {
              // Retrieve the user/role data from the XML file.
              ReadXmlDataStore();
              // Create a user object.
              XmlUserData user;
              // Test if the user name is in the dictionary of users.
              if (_XmlUserData.TryGetValue(userName, out user))
              {
                // Perform a case-sensitive comparison on the password.
                if (String.Compare(user.Password, userPassword, false) == 0)
                {
                  // Return true (authentication succeeded) if the passwords match.
                  return true;
                }
              }
            }
            catch (Exception ex)
            {
              // Raise an exception if an error occurs.
              throw new ProviderException(ex.Message);
            }
          }
          // Return false (authentication failed) if authentication fails to this point.
          return false;
        }
      
        bool IFtpRoleProvider.IsUserInRole(
             string sessionId,
             string siteName,
             string userName,
             string userRole)
        {
          // Validate that the user and role names are not empty.
          if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(userRole))
          {
            // Return false (role lookup failed) if either are empty.
            return false;
          }
          else
          {
            try
            {
              // Retrieve the user/role data from the XML file.
              ReadXmlDataStore();
              // Create a user object.
              XmlUserData user;
              // Test if the user name is in the dictionary of users.
              if (_XmlUserData.TryGetValue(userName, out user))
              {
                // Loop through the user's roles.
                foreach (string role in user.Roles)
                {
                  // Perform a case-insensitive comparison on the role name.
                  if (String.Compare(role, userRole, true) == 0)
                  {
                    // Return true (role lookup succeeded) if the role names match.
                    return true;
                  }
                }
              }
            }
            catch (Exception ex)
            {
              // Raise an exception if an error occurs.
              throw new ProviderException(ex.Message);
            }
          }
          // Return false (role lookup failed) if role lookup fails to this point.
          return false;
        }
      
        // Retrieve the user/role data from the XML file.
        private void ReadXmlDataStore()
        {
          // Lock the provider while the data is retrieved.
          lock (this)
          {
            try
            {
              // Test if the dictionary already has data.
              if (_XmlUserData.Count == 0)
              {
                // Create an XML document object and load the data XML file
                XPathDocument xmlDocument = new XPathDocument(_xmlFileName);
                // Create a navigator object to navigate through the XML file.
                XPathNavigator xmlNavigator = xmlDocument.CreateNavigator();
                // Loop through the users in the XML file.
                foreach (XPathNavigator node in xmlNavigator.Select("/Users/User"))
                {
                  // Retrieve a user name.
                  string userName = GetInnerText(node, "UserName");
                  // Retrieve the user's password.
                  string password = GetInnerText(node, "Password");
                  // Test if the data is empty.
                  if ((String.IsNullOrEmpty(userName) == false) && (String.IsNullOrEmpty(password) == false))
                  {
                    // Retrieve the user's roles.
                    string xmlRoles = GetInnerText(node, "Roles");
                    // Create a string array for the user roles.
                    string[] userRoles = new string[0];
                    // Test if the user has any roles defined.
                    if (String.IsNullOrEmpty(xmlRoles) == false)
                    {
                      // Split the roles by comma.
                      userRoles = xmlRoles.Split(',');
                    }
                    // Create a user data class.
                    XmlUserData userData = new XmlUserData(password, userRoles);
                    // Store the user data in the dictionary.
                    _XmlUserData.Add(userName, userData);
                  }
                }
              }
            }
            catch (Exception ex)
            {
              // Raise an exception if an error occurs.
              throw new ProviderException(ex.Message);
            }
          }
        }
      
        // Retrieve data from an XML element.
        private static string GetInnerText(XPathNavigator xmlNode, string xmlElement)
        {
          string xmlText = "";
          try
          {
            // Test if the XML element exists.
            if (xmlNode.SelectSingleNode(xmlElement) != null)
            {
              // Retrieve the text in the XML element.
              xmlText = xmlNode.SelectSingleNode(xmlElement).Value.ToString();
            }
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
          // Return the element text.
          return xmlText;
        }  
      }
      
      // Define the user data class.
      internal class XmlUserData
      {
        // Create a private string to hold a user's password.
        private string _password = "";
        // Create a private string array to hold a user's roles.
        private string[] _roles = new string[0];
      
        // Define the class constructor  requiring a user's password and roles array.
        public XmlUserData(string Password,string[] Roles)
        {
          this.Password = Password;
          this.Roles = Roles;
        }
      
        // Define the password property.
        public string Password
        {
          get { return _password; }
          set
          {
            try { _password = value; }
            catch (Exception ex)
            {
              throw new ProviderException(ex.Message);
            }
          }
        }
      
        // Define the roles property.
        public string[] Roles
        {
          get { return _roles; }
          set {
            try { _roles = value; }
            catch (Exception ex)
            {
              throw new ProviderException(ex.Message);
            }
          }
        }
      }
      
  5. Save and compile the project.

Note

If you did not use the optional steps to register the assemblies in the GAC, you will need to manually copy the assemblies to your IIS computer and add the assemblies to the GAC using the Gacutil.exe tool. For more information, see Gacutil.exe (Global Assembly Cache Tool).

Step 3: Add the Demo Provider to FTP

In this step, you will add the demo provider to your FTP service and the default Web site.

Adding the XML File

Create an XML file for the membership users and roles:

  • Paste the following code into a text editor:

    <Users>
       <User>
          <UserName>Alice</UserName>
          <Password>contoso!</Password>
          <EMail>alice@contoso.com</EMail>
          <Roles>Members,Administrators</Roles>
       </User>
       <User>
          <UserName>Bob</UserName>
          <Password>contoso!</Password>
          <EMail>bob@contoso.com</EMail>
          <Roles>Members</Roles>
       </User>
    </Users>
    
  • Save the code as "Users.xml" to your computer. For example, you could use the path C:\Inetpub\XmlSample\Users.xml.

Note

For security reasons, this file should not be stored in a folder that is located in your Web site's content area.

Adding the Provider

  1. Determine the assembly information for the extensibility provider:

    • In Windows Explorer, open your C:\Windows\assembly path, where C: is your operating system drive.
    • Locate the FtpXmlAuthentication assembly.
    • Right-click the assembly, and then click Properties.
    • Copy the Culture value; for example: Neutral.
    • Copy the Version number; for example: 1.0.0.0.
    • Copy the Public Key Token value; for example: 426f62526f636b73.
    • Click Cancel.
  2. Using the information from the previous steps, add the extensibility provider to the global list of FTP providers and configure the options for the provider:

    • At the moment there is no user interface that enables you to add properties for a custom authentication module, so you will have to use the following command line:

      cd %SystemRoot%\System32\Inetsrv
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpXmlAuthentication',type='FtpXmlAuthentication,FtpXmlAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthentication']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthentication'].[key='xmlFileName',value='C:\Inetpub\XmlSample\Users.xml']" /commit:apphost
      

    Note

    The file path that you specify in the xmlFileName attribute must match the path where you saved the "Users.xml" file on your computer in the earlier in this walkthrough.

  3. Add the custom authentication provider for an FTP site:

    • Open an FTP site in the Internet Information Services (IIS) Manager.
    • Double-click FTP Authentication in the main window.
    • Click Custom Providers... in the Actions pane.
    • Check FtpXmlAuthentication in the providers list.
    • Click OK.
  4. Add an authorization rule for the authentication provider:

    • Double-click FTP Authorization Rules in the main window.

    • Click Add Allow Rule... in the Actions pane.

    • You can add either of the following authorization rules:

      -For a specific user:

      • Select Specified users for the access option.
      • Enter the user name. For example, using the XML sample in this walkthrough you could enter "Alice" or "Bob".

      -For a role or group:

      • Select Specified roles or user groups for the access option.
      • Enter the role or group name. For example, using the XML sample in this walkthrough you could enter "Members" or "Administrators".

      -Select Read and/or Write for the Permissions option.

    • Click OK.

Summary

In this walkthrough you learned how to:

  • Create a project in Visual Studio 2008 for a custom FTP authentication provider.
  • Implement the extensibility interface for custom FTP authentication.
  • Add a custom authentication provider to your FTP service.

When users connect to your FTP site, the FTP service will attempt to authenticate users with your custom authentication provider. If this fails, the FTP service will use other built-in or authentication providers to authenticate users.