HttpException: An error occurred while attempting to impersonate

Pasados más de tres meses desde mi último post, ya es momento de retomarlo. Hoy voy a escribir sobre un problema con el que me he encontrado en varias ocasiones en los últimos meses. Básicamente, el problema se produce cuando tenemos una aplicación ASP.NET configurada para impersonar al usuario autenticado, y en algún punto de la aplicación se trata de impersonar programáticamente a un usuario distinto. En estas circunstancias, la aplicación va a fallar con el siguiente error:

Exception Details:

System.Web.HttpException: An error occurred while attempting to impersonate. Execution of this request cannot continue.

Stack Trace:

[HttpException (0x80004005): An error occurred while attempting to impersonate. Execution of this request cannot continue.]

System.Web.ImpersonationContext.GetCurrentToken() +8845961

System.Web.ImpersonationContext.get_CurrentThreadTokenExists() +58

System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +193

System.Web.ApplicationStepManager.ResumeSteps(Exception error) +501

System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData) +123

System.Web.HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr) +379

Básicamente, el problema radica en que al llamar al método Impersonate (o llamando directamente a la API LogonUser que es la que se termina llamando desde Impersonate), se intenta acceder al token del proceso (por ejemplo, w3wp.exe), y la cuenta del usuario autenticado (el que está siendo impersonado debido a la configuración del web.config) no tiene permiso para acceder a dicho token.

Por lo tanto, para poder impersonar una cuenta distinta programáticamente, necesitamos deshacer la impersonación inicial antes de impersonar una cuenta distinto. Para ello, podemos llamar a la API RevertToSelf para deshacer la impersonación antes de llamar a LogonUser.

En el siguiente ejemplo se ilustra cómo podríamos hacerlo:

const int LOGON32_PROVIDER_DEFAULT = 0;

const int LOGON32_LOGON_INTERACTIVE = 2;

const int SecurityImpersonation = 2;

int win32ErrorNumber = 0;

//Mostramos por pantalla la identidad inicial (la impersonada mediante en el web.config):

Response.Write("Identidad inicial: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");

//Guardamos esa identidad inicial para poder restaurarlo posteriormente

WindowsIdentity _initialIdentity = WindowsIdentity.GetCurrent();

//Llamamos a la API "RevertToSelf" para deshacer la impersonación configurada en el web.config:

RevertToSelf();

//Volvemos a mostrar la identidad, y ahora vemos que ha cambiado a la identidad del application

//pool: (NETWORK SERVICE) por defecto.

Response.Write("Después de llamar a RevertToSelf: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");

//Realizamos la impersonación deseada.

IntPtr _tokenHandle = IntPtr.Zero;

IntPtr _dupeTokenHandle = IntPtr.Zero;

string _domainname = "CONTOSO";

string _username = "demo";

string _password = "P@ssw0rd!";

if (!LogonUser(_username, _domainname, _password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out _tokenHandle))

{

    win32ErrorNumber = System.Runtime.InteropServices.Marshal.GetLastWin32Error();

    throw new Exception(win32ErrorNumber.ToString());

}

if (!DuplicateToken(_tokenHandle, SecurityImpersonation, out _dupeTokenHandle))

{

    win32ErrorNumber = System.Runtime.InteropServices.Marshal.GetLastWin32Error();

    CloseHandle(_tokenHandle);

    throw new Exception(win32ErrorNumber.ToString());

}

System.Security.Principal.WindowsIdentity newId = new System.Security.Principal.WindowsIdentity(_dupeTokenHandle);

WindowsImpersonationContext _impersonatedUser = newId.Impersonate();

//Mostramos por pantalla la identidad impersonada

Response.Write("Después de impersonar: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");

//Desahacemos la impersonación cuando ya no lo necesitemos.

_impersonatedUser.Undo();

//Mostramos por pantalla la identidad, que otra vez será la del application pool

Response.Write("Deshacer impersonación: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");

//Volvemos a impersonar con la identidad inicial (la del web.config) que hemos mantenido referenciada

//con la variable _impersonatedUser:

_impersonatedUser = _initialIdentity.Impersonate();

//Por último volvemos a mostrar la identidadMostramos por pantalla la identidad

Response.Write("Volver a impersonación inicial: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");

Tras ejecutar el código anterior, el resultado emitido por pantalla sería el siguiente (en mi ejemplo):

Identidad inicial: CONTOSO\daniel
Después de llamar a RevertToSelf: NT AUTHORITY\NETWORK SERVICE
Después de impersonar: CONTOSO\demo
Deshacer impersonación: NT AUTHORITY\NETWORK SERVICE
Volver a impersonación inicial: CONTOSO\daniel

Estas son las signatures de C# para llamar a las APIs del ejemplo mediante platform invoke:

[DllImport("advapi32.dll", SetLastError = true)]

public static extern bool LogonUser(

    string lpszUsername,

    string lpszDomain,

    string lpszPassword,

    int dwLogonType,

    int dwLogonProvider,

    out IntPtr phToken);

[DllImport("advapi32.dll", SetLastError = true)]

static extern bool RevertToSelf();

[DllImport("advapi32.dll", SetLastError = true)]

public extern static bool DuplicateToken(

    IntPtr ExistingTokenHandle,

    int SECURITY_IMPERSONATION_LEVEL,

    out IntPtr DuplicateTokenHandle);

[DllImport("kernel32.dll", SetLastError = true)]

[return: MarshalAs(UnmanagedType.Bool)]

static extern bool CloseHandle(IntPtr hObject);

Espero que os sea de utilidad.

- Daniel Mossberg