I do a lot of server programming. I frequently need to be able to run some code as a different user than the currently logged-in user. Unfortunately, the .Net Framework does not make this particularly easy. Sure, there is the WindowsIdentity class and its Impersonate method. But how do you create an instance of a WindowsIdentity object? You need to pass the constructor a logon token. How do you get a logon token? Well, .Net is rather silent on that point.

PInvoke is your friend

The Win32 API does have a LogonUser function in the advapi32.dll DLL. Thanks to the wonderful work of PInvoke (now part of RedGate), you can easily get the signature of the LogonUser function translated into .Net. It looks like this:

[DllImport("advapi32.dll", SetLastError = true)]  
public static extern bool LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);

I found a nice code snippet by Viji RAJKUMAR that showed how to use the LogonUser function, handle the Win32 error codes, and handle the .Net impersonation contexts. But do you really want deal with all of that whenever you have to run some code as someone else? I sure don’t.

Inspiration from SharePoint’s RunWithElevatedPrivileges

One thing that the SharePoint API has is a very handy method on the SPSecurity class called RunWithElevatedPrivileges. RunWithElevatedPrivileges takes a function pointer (called a delegate in .Net) and executes the code in the function in the context of a pre-defined user identity (the identity of the IIS application pool in which the code is running). I really liked the ease of invocation of RunWithElevatedPrivileges method, but I needed to have a method that allowed me to specify the user name and password of any arbitrary user account.

The RunAs method is born

I used Viji’s code snippet as a starting point, and enhanced it to accept a delegate function. The result is that I can easily call RunAs, passing in a username and password, and a block of code I want executed under that user’s identity. Calling RunAs looks like this:

   1:  BlackBlade.Utilities.SecurityUtilities.RunAs(delegate()
   2:      {
   3:          File.Move(“local_file”, “network_file_location”);
   4:      },
   5:      “a_username”,
   6:      “some_password”);

Line 3 in the above code snippet is the code that will be executed with the credentials supplied to the RunAs method. You can have as many lines of code between lines 2 and 4. And those lines of code can interact with variables outside the code block. The RunAs method takes care of the details of interoperating with the Win32 API and handling the impersonation and reversion of .Net security contexts.

RunAs implemented

Here’s the full implementation of the RunAs method. Less than 75 lines of code. You can compile this as an assembly and reference it form other projects. It’s very handy.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:   
   5:  using System.Security.Principal;
   6:  using System.Runtime.InteropServices;
   7:   
   8:  namespace BlackBlade.Utilities
   9:  {
  10:      public class SecurityUtilities
  11:      {
  12:          [DllImport("advapi32.dll", SetLastError = true)]
  13:          private static extern bool LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);
  14:          public delegate void RunAsDelegate();
  15:   
  16:          public static void RunAs(RunAsDelegate MethodToRunAs, string Username, string Password)
  17:          {
  18:              string userName;
  19:              string domain;
  20:   
  21:              if (Username.IndexOf('\') > 0)
  22:              {
  23:                  //a domain name was supplied
  24:                  string[] usernameArray = Username.Split('\');
  25:                  userName = usernameArray[1];
  26:                  domain = usernameArray[0];
  27:              }
  28:              else
  29:              {
  30:                  //there was no domain name supplied
  31:                  userName = Username;
  32:                  domain = ".";
  33:              }
  34:   
  35:              RunAs(MethodToRunAs, userName, Password, domain);
  36:          }
  37:   
  38:          public static void RunAs(RunAsDelegate MethodToRunAs, string Username, string Password, string Domain)
  39:          {
  40:              IntPtr imp_token;
  41:              WindowsIdentity wid_admin = null;
  42:              WindowsImpersonationContext wic = null;
  43:   
  44:              try
  45:              {
  46:                  if (LogonUser(Username, string.IsNullOrEmpty(Domain) ? "." : Domain, Password, 9, 0, out imp_token))
  47:                  {
  48:                      //the impersonation suceeded
  49:                      wid_admin = new WindowsIdentity(imp_token);
  50:                      wic = wid_admin.Impersonate();
  51:   
  52:                      //run the delegate method
  53:                      MethodToRunAs();
  54:                  }
  55:                  else
  56:                      throw new Exception(string.Format("Could not impersonate user {0} in domain {1} with the specified password.", Username, Domain));
  57:              }
  58:              catch (Exception se)
  59:              {
  60:                  int ret = Marshal.GetLastWin32Error();
  61:                  if (wic != null)
  62:                      wic.Undo();
  63:   
  64:                  throw new Exception("Error code: " + ret.ToString(), se);
  65:              }
  66:              finally
  67:              {
  68:                  //revert to self
  69:                  if (wic != null)
  70:                      wic.Undo();
  71:              }
  72:          }
  73:      }
  74:  }

Line 53 in the previous code block invokes the caller-supplied code that should be run unsung the new impersonation context.

Technorati Tags: .Net,C#,Impersonation,LogonUser