A Personable Form Of Impersonation

Have you tried impersonating a different user when executing code in your ASP.NET sites? Yeah, you can use a bunch of options in the web and machine config files but those might not give you enough control. Whats more you still have to contend with the way IIS handles credentials with the worker process then connecting user... er... or was that connecting user then... ah... never mind...

Executing a block of code with a specific set of credentials is a very handy way to have precise control over your application. Recently, I was working on copying files from an ASP.NET website to a remote UNC path. I needed to perform the transfer using different credentials for different servers... yeah, it was ugly but required...

Below is the code I finally ended up with...

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text.RegularExpressions;

namespace Samples {

    /// <summary>
    /// Allows you to execute code with an alternate set of credentials
    /// </summary>
    public class ImpersonationContext : IDisposable {

        #region Imported Methods

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool RevertToSelf();

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int DuplicateToken(
            IntPtr token, 
            int impersonationLevel, 
            ref IntPtr newToken
            );

        [DllImport("advapi32.dll")]
        private static extern int LogonUserA(
            string username,
            string domain,
            string password,
            int logonType,
            int logonProvider,
            ref IntPtr token
            );

        #endregion

        #region Constants

        private const int INTERACTIVE_LOGON = 2;
        private const int DEFAULT_PROVIDER = 0;

        private const string REGEX_GROUP_USERNAME = "username";
        private const string REGEX_GROUP_DOMAIN = "domain";
        private const string REGEX_EXTRACT_USER_INFO =
            @"^(?<domain>[^\\]+)\\(?<username>.*)$|^(?<username>[^@]+)@(?<domain>.*)$";

        private const string EXCEPTION_COULD_NOT_IMPERSONATE = 
            "Could not impersonate user '{0}'.";
        private const string EXCEPTION_COULD_NOT_PARSE_USERNAME = 
            "Cannot determine username and domain from '{0}'";

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new Impersonation context
        /// </summary>
        public ImpersonationContext(string fullUsername, string password) {
            this.SetCredentials(fullUsername, password);
        }

        /// <summary>
        /// Creates a new Impersonation context
        /// </summary>
        public ImpersonationContext(string username, string domain, string password) {
            this.SetCredentials(username, domain, password);
        }

        #endregion

        #region Static Creation

        /// <summary>
        /// Executes a set of code using the credentials provided
        /// </summary>
        public static void Execute(string fullUsername, string password, Action action) {
            using (ImpersonationContext context = 
        new ImpersonationContext(fullUsername, password)) {
                context.Execute(action);
            }
        }

        /// <summary>
        /// Executes a set of code using the credentials provided
        /// </summary>
        public static void Execute(string username, string domain, string password, 
      Action action) {
            using (ImpersonationContext context = 
        new ImpersonationContext(username, domain, password)) {
                context.Execute(action);
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// The username for this connection
        /// </summary>
        public string Username { get; private set; }

        /// <summary>
        /// The domain name for this user
        /// </summary>
        public string Domain { get; private set; }

        /// <summary>
        /// The identity of the executing account
        /// </summary>
        public WindowsIdentity Identity { get; private set; }

        //connection details
        private string _Password;
        private WindowsImpersonationContext _Context;

        #endregion

        #region Private Methods

        /// <summary>
        /// Begins to impersonate the provided credentials
        /// </summary>
        public bool BeginImpersonation() {

            //create the token containers
            IntPtr token = IntPtr.Zero;
            IntPtr tokenDuplicate = IntPtr.Zero;

            if (ImpersonationContext.RevertToSelf()) {

                //attempt the login
                int success = ImpersonationContext.LogonUserA(
                    this.Username,
                    this.Domain,
                    this._Password,
                    INTERACTIVE_LOGON,
                    DEFAULT_PROVIDER,
                    ref token
                    );

                //if this worked, perform the impersonation
                if (success != 0) {

                    int duplicate = ImpersonationContext.DuplicateToken(
            token, 2, ref tokenDuplicate);
                    if (duplicate != 0) {

                        //assign the identity to use
                        //this.Identity = new WindowsIdentity(tokenDuplicate);
                        this._Context = WindowsIdentity.Impersonate(tokenDuplicate);
                        if (this._Context != null) {
                            ImpersonationContext.CloseHandle(token);
                            ImpersonationContext.CloseHandle(tokenDuplicate);
                            return true;
                        }

                    }
                }
            }

            //close the tokens if required
            if (token != IntPtr.Zero) ImpersonationContext.CloseHandle(token);
            if (tokenDuplicate != IntPtr.Zero)
        ImpersonationContext.CloseHandle(tokenDuplicate);

            //return this failed
            return false;
        }

        /// <summary>
        /// Ends impersonating the current request
        /// </summary>
        public void EndImpersonation() {
            if (this._Context is WindowsImpersonationContext)
        this._Context.Undo();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Accepts a full domain and assigns it for this connection
        /// </summary>
        public void SetCredentials(string fullUsername, string password) {

            //extract the user information
            Match user = Regex.Match(fullUsername, REGEX_EXTRACT_USER_INFO);
            if (!user.Success) {
                string message = string.Format(
          EXCEPTION_COULD_NOT_PARSE_USERNAME, 
          fullUsername);
                throw new ArgumentException(message);
            }

            //extract the values
            string username = user.Groups[REGEX_GROUP_USERNAME].Value;
            string domain = user.Groups[REGEX_GROUP_DOMAIN].Value;

            //update the credentials
            this.SetCredentials(username, domain, password);

        }

        /// <summary>
        /// Changes the credentials for this connection to use
        /// </summary>
        public void SetCredentials(string username, string domain, string password) {
            this.Username = username;
            this.Domain = domain;
            this._Password = password;
        }

        /// <summary>
        /// Executes the action using the credentials provided
        /// </summary>
        public void Execute(Action action) {

            //perform the requested action
            if (this.BeginImpersonation()) {
                try {
                    action();
                }
                finally {
                    this.EndImpersonation();
                }
            }
            //since this couldn't login, give up
            else {
                string message = string.Format(
          EXCEPTION_COULD_NOT_IMPERSONATE, 
          this.Username);
                throw new OperationCanceledException(message);
            }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Performs any cleanup work
        /// </summary>
        public void Dispose() {
            this.EndImpersonation();
            if (this._Context is WindowsImpersonationContext)
        this._Context.Dispose();
        }

        #endregion

    }

}

Nothing like a lot of code to help fill in the blanks of a blog post, huh?

After reading a dozen blog post, tutorials and wikis, this is what I finally came up with - a nice simple way to execute a block of code without needing to worry about a lot of setup options. How about a few examples?

//who is running the code now
string who = WindowsIdentity.GetCurrent().Name;

//the credentials to use 
string username = "domain\\hugoware";
string password = "passw0rd";

//create a context inside of a using statement
//or simply create it and dispose it yourself
using(ImpersonationContext context = new ImpersonationContext(username, password)) {

    //execute using a anonymous method
    context.Execute(() => {
        who = WindowsIdentity.GetCurrent().Name;
    });

    //or manage it yourself
    context.BeginImpersonation();
    who = WindowsIdentity.GetCurrent().Name;
    context.EndImpersonation(); //optional if inside of a 'using'

}

//or use static methods
ImpersonationContext.Execute(username, password, () => {
    who = WindowsIdentity.GetCurrent().Name;
});

At this point I'm not exactly sure what this means for code that isn't exactly straight forward, for example Threading or executing code when the credentials don't have any access to the machine. I'll follow up later as I find out more.

March 25, 2010

A Personable Form Of Impersonation

Post titled "A Personable Form Of Impersonation"