How to Use Managed Code (C#) to Create an FTP Authentication and Authorization Provider using an XML Database

By Robert McMurray

April 10, 2012

[This documentation is preliminary and is subject to change.]

Compatibility

Version Notes
IIS 8.0 The FTP 8.0 service is required for custom authorization.
IIS 7.5 Custom authorization is not supported in FTP 7.5.
IIS 7.0 Custom authorization is not supported in FTP 7.0.

Note: The FTP 8.0 service ships as a feature for IIS 8.0 in Windows 8 and Windows 8 Server.

Introduction

Microsoft has created a new FTP 8.0 service for Windows Server® 2012 that builds upon the rich set of features that were introduced in FTP 7.0 and FTP 7.5. In addition, this new FTP service extends the list of Application Programming Interfaces (APIs) for FTP with new extensibility features like support for custom authorization and simple event processing.

This walkthrough will lead you through the steps to use managed code to an FTP authentication and authorization provider that uses the sample XML file from the following walkthrough for users and roles, with the necessary changes that are required to support custom authorization rules:

How to use the Sample Read-Only XML Membership and Role Providers with IIS

In This Walkthrough

Prerequisites

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

  1. IIS 8 must be installed on your Windows Server 8 server, and the Internet Information Services (IIS) Manager must also be installed.
  2. The FTP 8 service must be installed.
  3. You must have FTP publishing enabled for a site.
  4. You should use Visual Studio 2008 or later. (Note: You can use a different version of Visual Studio, but some of the steps in this walkthrough may be different.)

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 for the demo provider.

  1. Open Microsoft Visual Studio 2008 or later.
  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 FtpXmlAuthorization 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 FtpXmlAuthorization 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.
      • C:\Program Files\Reference Assemblies\Microsoft\IIS
    • Click Add Folder.
  5. Add a strong name key to the project:
    • Click Project, and then click FtpXmlAuthorization Properties.
    • Click the Signing tab.
    • Check the Sign the assembly check box.
    • Choose <New...> from the strong key name drop-down box.
    • Enter FtpXmlAuthorizationKey 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 FtpXmlAuthorization 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 and authorization 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 and authorization provider class.
      public class FtpXmlAuthorization :
      BaseProvider,
      IFtpAuthenticationProvider,
      IFtpAuthorizationProvider,
      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;
      }

      // Define the IsUserInRole method.
      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;
      }

      // Define the AuthorizeUser method.
      FtpAccess IFtpAuthorizationProvider.GetUserAccessPermission(
      string pszSessionId,
      string pszSiteName,
      string pszVirtualPath,
      string pszUserName)
      {
      // Define the default access.
      FtpAccess _ftpAccess = FtpAccess.None;

      // Validate that the user and virtual path are not empty.
      if (String.IsNullOrEmpty(pszUserName) || String.IsNullOrEmpty(pszVirtualPath))
      {
      // Return false (authorization failed) if either are empty.
      return FtpAccess.None;
      }
      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(pszUserName, out user))
      {
      // Loop through the user's roles.
      foreach (KeyValuePair<string, FtpAccess> rule in user.Rules)
      {
      // Test if the virtual path matches an authorization rule.
      // Note: This is a very simple path search.
      if (pszVirtualPath.StartsWith(rule.Key))
      {
      _ftpAccess = rule.Value;
      }
      }
      }
      }
      catch (Exception ex)
      {
      // Raise an exception if an error occurs.
      throw new ProviderException(ex.Message);
      }
      }

      return _ftpAccess;
      }

      // 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 dictionary to hold the user's authorization rules.
      Dictionary<string, FtpAccess> userRules =
      new Dictionary<string, FtpAccess>(
      StringComparer.InvariantCultureIgnoreCase);
      // Loop through the set of authorization rules for the user.
      foreach (XPathNavigator rule in node.Select("Rules/Rule"))
      {
      // Retrieve the URL path for the authorization rule.
      string xmlPath = rule.GetAttribute("path", string.Empty);
      // Strip trailing slashes from paths.
      if (xmlPath.EndsWith("/") && xmlPath.Length > 1)
      xmlPath = xmlPath.Substring(0, xmlPath.Length - 1);
      // Retrieve the user's permissionsfor the authorization rule.
      string xmlPermissions = rule.GetAttribute("permissions", string.Empty);
      // Parse the FTP access permissions for the authorization rule.
      FtpAccess userPermissions = FtpAccess.None;
      switch (xmlPermissions.Replace(" ", "").ToLower())
      {
      case "read":
      userPermissions = FtpAccess.Read;
      break;
      case "write":
      userPermissions = FtpAccess.Write;
      break;
      case "read,write":
      case "write,read":
      userPermissions = FtpAccess.ReadWrite;
      break;
      default:
      userPermissions = FtpAccess.None;
      break;
      }
      // Add the authorization rule to the dictionary.
      userRules.Add(xmlPath, userPermissions);
      }
      // Create a user data class.
      XmlUserData userData = new XmlUserData(password, userRoles, userRules);
      // 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];
      // Create a private dictionary to hold user authorization rules.
      private Dictionary<string, FtpAccess> _rules =
      new Dictionary<string, FtpAccess>(
      StringComparer.InvariantCultureIgnoreCase);

      // Define the class constructor requiring a user's password and roles array.
      public XmlUserData(string Password, string[] Roles, Dictionary<string, FtpAccess> Rules)
      {
      this.Password = Password;
      this.Roles = Roles;
      this.Rules = Rules;
      }

      // 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);
      }
      }
      }

      // Define the rules property.
      public Dictionary<string, FtpAccess> Rules
      {
      get { return _rules; }
      set
      {
      try { _rules = 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 the following topic on the Microsoft MSDN Web site:

Global Assembly Cache Tool (Gacutil.exe)

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>P@ssw0rd</Password>
    <EMail>alice@contoso.com</EMail>
    <Roles>Members,Administrators</Roles>
    <Rules>
    <Rule path="/" permissions="Read,Write" />
    </Rules>
    </User>
    <User>
    <UserName>Bob</UserName>
    <Password>P@ssw0rd</Password>
    <EMail>bob@contoso.com</EMail>
    <Roles>Members</Roles>
    <Rules>
    <Rule path="/" permissions="Read" />
    <Rule path="/bob" permissions="Read,Write" />
    </Rules>
    </User>
    <User>
    <UserName>Carol</UserName>
    <Password>P@ssw0rd</Password>
    <EMail>carol@contoso.com</EMail>
    <Roles>Members</Roles>
    <Rules>
    <Rule path="/" permissions="Write" />
    <Rule path="/carol" permissions="Read,Write" />
    </Rules>
    </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 FtpXmlAuthorization 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 custom authentication or authorization modules, so you will have to use the following command line:
      cd %SystemRoot%\System32\Inetsrv

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpXmlAuthorization',type='FtpXmlAuthorization,FtpXmlAuthorization,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization']" /commit:apphost

      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization'].[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. Specify 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 FtpXmlAuthorization in the providers list.
    • Click OK.
  4. Specify the custom authorization provider for an FTP site:
    • Open an FTP site in the Internet Information Services (IIS) Manager.
    • Double-click FTP Authorization Rules in the main window.
    • Click Edit Feature Settings... in the Actions pane.
    • Click to select Choose a custom authorization provider.
    • Select FtpXmlAuthorization in the drop-down menu.
    • Click OK.

Summary

In this walkthrough you learned how to:

  • Create a project in Visual Studio for a custom FTP authentication and authorization provider.
  • Implement the extensibility interfaces for custom FTP authentication and authorization.
  • Add a custom authentication and authorization 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. After users have been authenticated, the FTP service will call the custom authorization provider to authorize users. If this succeeds, the users will be granted access to your server. If this fails, users will not be able to access your server.



Discuss in IIS Forums