El token antifalsificación está destinado al usuario "" pero el usuario actual es "nombre de usuario"

130

Estoy creando una aplicación de página única y estoy experimentando un problema con los tokens antifalsificación.

Sé por qué ocurre el problema, pero no sé cómo solucionarlo.

Me sale el error cuando sucede lo siguiente:

  1. El usuario que no ha iniciado sesión carga un cuadro de diálogo (con un token antifalsificación generado)
  2. El usuario cierra el diálogo.
  3. El usuario inicia sesión
  4. El usuario abre el mismo cuadro de diálogo.
  5. El usuario envía el formulario en el cuadro de diálogo

El token antifalsificación está destinado al usuario "" pero el usuario actual es "nombre de usuario"

La razón por la que esto sucede es porque mi aplicación es 100% de una sola página, y cuando un usuario inicia sesión con éxito a través de una publicación ajax /Account/JsonLogin, simplemente cambio las vistas actuales con las "vistas autenticadas" devueltas desde el servidor pero no vuelvo a cargar el página.

Sé que esta es la razón porque si simplemente vuelvo a cargar la página entre los pasos 3 y 4, no hay ningún error.

Por lo tanto, parece que @Html.AntiForgeryToken()en el formulario cargado aún se devuelve un token para el usuario anterior hasta que se vuelve a cargar la página.

¿Cómo puedo cambiar @Html.AntiForgeryToken()para devolver un token para el nuevo usuario autenticado?

Inyecto un nuevo GenericalPrincipalcon una costumbre IIdentitycada Application_AuthenticateRequestvez que me @Html.AntiForgeryToken()llaman HttpContext.Current.User.Identity, de hecho, mi Identidad personalizada con la IsAuthenticatedpropiedad establecida en verdadero y @Html.AntiForgeryTokenaún parece representar un token para el usuario anterior a menos que haga una recarga de la página.

parlamento
fuente
¿Puede verificar que se llame al código @ Html.AntiForgeryToken sin volver a cargarlo?
Kyle C
Definitivamente lo es, puedo romper con éxito allí para inspeccionar HttpContext.Current.User objeto como mencioné
parlamento
2
Consulte esto: stackoverflow.com/a/19471680/193634
Rosdi Kasim el
@parliament, ¿podría decir qué opción eligió en la respuesta a continuación?
Siddharth Pandey
Creo que hice una excepción para ir con una recarga completa si no recuerdo mal. Pero espero encontrar este problema muy pronto en un nuevo proyecto. Volveré a publicar si opto por una mejor opción de trabajo.
Parlamento

Respuestas:

170

Esto sucede porque el token antifalsificación incorpora el nombre de usuario del usuario como parte del token cifrado para una mejor validación. Cuando llama por primera vez, @Html.AntiForgeryToken()el usuario no está conectado, por lo que el token tendrá una cadena vacía para el nombre de usuario, después de que el usuario inicie sesión, si no reemplaza el token antifalsificación, no pasará la validación porque el token inicial era para usuario anónimo y ahora tenemos un usuario autenticado con un nombre de usuario conocido.

Tiene algunas opciones para resolver este problema:

  1. Esta vez, deje que su SPA realice una POST completa y cuando la página se vuelva a cargar tendrá un token antifalsificación con el nombre de usuario actualizado incrustado.

  2. Tenga una vista parcial con justo @Html.AntiForgeryToken()y justo después de iniciar sesión, haga otra solicitud AJAX y reemplace su token antifalsificación existente con la respuesta de la solicitud.

  3. Simplemente deshabilite la verificación de identidad que realiza la validación antifalsificación. Añadir lo siguiente a su Application_Start método: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.

epignosisx
fuente
21
@parlamento: aceptó esta respuesta, ¿podría compartir con nosotros qué opción eligió?
R. Schreurs
9
+1 para la opción agradable y simple 3. Los cierres de sesión programados por los proveedores de OAuth también causan este problema.
Gone Coding
18
La opción 3 no funcionó para mí. Mientras estaba desconectado, abrí dos ventanas en la página de inicio de sesión. Ingresó como un usuario en una ventana, luego inició sesión como otro usuario en la otra y recibió el mismo error.
McGaz
55
Desafortunadamente, no pude encontrar una buena solución para esto. Eliminé el token de la página de inicio de sesión. Todavía lo incluyo en las publicaciones después de iniciar sesión.
McGaz
77
La opción 3 tampoco funcionó para mí. Todavía obteniendo el mismo error.
Joao Leme
25

Para corregir el error, debe colocar la OutputCacheAnotación de datos en la ActionResultpágina Obtener inicio de sesión como:

[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
public ActionResult Login(string returnUrl)
usuario3401354
fuente
3
Esto resolvió el problema para mí, tiene mucho sentido. ¡Gracias!
Prime03
Mi caso de uso fue que el usuario intentó iniciar sesión y se mostró un error, por ejemplo, "cuenta deshabilitada" a través de ModelState.AddError (). Luego, si volvían a hacer clic en iniciar sesión, verían este error. Sin embargo, esta solución solo les dio una nueva vista de inicio de sesión en blanco nuevamente en lugar del error de token anti falsificación. Entonces, no es una solución.
yourpublicdisplayname
Mi caso: 1. Inicio de sesión de usuario () y aterriza en la página de inicio. 2. El usuario presiona el botón Atrás y vuelve a la vista de Inicio de sesión. 3. El usuario vuelve a iniciar sesión y ve el error "El token antifalsificación está destinado al usuario" "pero el usuario actual es" nombre de usuario "" En la página de error Si el usuario hace clic en cualquier otra pestaña del menú, la aplicación funcionaba como se esperaba . El uso del código anterior del usuario aún puede presionar el botón Atrás, pero se redirige a la página de inicio. Entonces, no importa cuántas veces el usuario presione el botón Atrás, lo redirigirá a la página de inicio. Gracias
Ravi
¿Alguna idea de por qué esto no funciona en una webview de Xamarin?
Noobie3001
1
Para ver una explicación completa mejora el rendimiento con el almacenamiento en caché de salida
stomy
8

Tuve el mismo problema, y ​​este truco sucio lo solucionó, al menos hasta que pueda solucionarlo de una manera más limpia.

    public ActionResult Login(string returnUrl)
    {
        if (AuthenticationManager.User.Identity.IsAuthenticated)
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Login");
        }

...

mnemotécnica
fuente
1
Parece que tuve el mismo problema. En mi opinión, no es un truco, es algo más común que todos debemos verificar al iniciar sesión. Si el usuario ya ha iniciado sesión, simplemente cierre la sesión y muestre la página de inicio de sesión. Arreglado mi problema, gracias.
Alexandre
7

El mensaje aparece cuando inicia sesión cuando ya está autenticado.

Este ayudante hace exactamente lo mismo que el [ValidateAntiForgeryToken]atributo.

System.Web.Helpers.AntiForgery.Validate()

Retire el [ValidateAntiForgeryToken]atributo del controlador y coloque este método de ayuda en acción.

Entonces, cuando el usuario ya esté autenticado, redirija a la página de inicio o, si no, continúe con la verificación del token antifalsificación válido después de esta verificación.

if (User.Identity.IsAuthenticated)
{
    return RedirectToAction("Index", "Home");
}

System.Web.Helpers.AntiForgery.Validate();

Para intentar reproducir el error, proceda de la siguiente manera: si está en su página de inicio de sesión y no está autenticado. Si duplica la pestaña e inicia sesión con la segunda pestaña. Y si vuelve a la primera pestaña de la página de inicio de sesión e intenta iniciar sesión sin volver a cargar la página ... tiene este error.

A. Morel
fuente
Excelente solucion! Esto resolvió mi problema después de probar muchas otras sugerencias que no funcionaron. En primer lugar, fue un error reproducir el error, hasta que descubrí que podría deberse a que 2 navegadores o pestañas se abren con la misma página, y el usuario inicia sesión desde una, y luego inicia sesión desde la segunda sin volver a cargar.
Nicki
Gracias por esta solución A mí también me funcionó. Agregué una verificación para ver si la Identidad era la misma que el nombre de usuario de inicio de sesión, y si es así, felizmente seguiré intentando iniciar sesión y cerrar sesión si no es así. Por ejemplo, intente {System.Web.Helpers.AntiForgery.Validate ();} catch (HttpAntiForgeryException) {if (! User.Identity.IsAuthenticated || string.Compare (User.Identity.Name, model.Username)! = 0) {// Su lógica de desconexión aquí}}
Steve Owen
2

Tengo la misma excepción que ocurre la mayor parte del tiempo en el servidor de producción.

¿Por que sucede?

Ocurre cuando el usuario inicia sesión con credenciales válidas y una vez que inicia sesión y redirige a otra página, y después de presionar el botón Atrás se mostrará la página de inicio de sesión y nuevamente ingresó credenciales válidas esa vez se producirá esta excepción.

¿Cómo resolver?

Simplemente agregue esta línea y funcione perfecto, no recibirá un error.

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")]
Brijesh Mavani
fuente
1

Tuve un problema bastante específico pero similar dentro del proceso de registro. Una vez que el usuario hizo clic en el enlace de correo electrónico que se le envió, iniciará sesión y se lo enviará directamente a la pantalla de detalles de la cuenta para completar más información. Mi código fue:

    Dim result = Await UserManager.ConfirmEmailAsync(userId, code)
    If result.Succeeded Then
        Dim appUser = Await UserManager.FindByIdAsync(userId)
        If appUser IsNot Nothing Then
            Dim signInStatus = Await SignInManager.PasswordSignInAsync(appUser.Email, password, True, shouldLockout:=False)
            If signInStatus = SignInStatus.Success Then
                Dim identity = Await UserManager.CreateIdentityAsync(appUser, DefaultAuthenticationTypes.ApplicationCookie)
                AuthenticationManager.SignIn(New AuthenticationProperties With {.IsPersistent = True}, identity)
                Return View("AccountDetails")
            End If
        End If
    End If

Descubrí que la Vista de retorno ("AccountDetails") me estaba dando la excepción del token, supongo que porque la función ConfirmEmail estaba decorada con AllowAnonymous pero la función AccountDetails tenía ValidateAntiForgeryToken.

Cambiar el retorno a retorno RedirectToAction ("AccountDetails") resolvió el problema para mí.

Liam
fuente
1
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]

public ActionResult Login(string returnUrl)

Puede probar esto colocando un punto de interrupción en la primera línea de su acción Iniciar sesión (Obtener). Antes de agregar la directiva OutputCache, el punto de interrupción se alcanzaría en la primera carga, pero después de hacer clic en el botón Atrás del navegador no lo haría. Después de agregar la directiva, debe terminar con el punto de interrupción golpeado cada vez, por lo que AntiForgeryToken será el núcleo, no el vacío.

Marian Dalalau
fuente
0

Tuve el mismo problema con una aplicación ASP.NET MVC Core de una sola página. Lo resolví configurando HttpContext.Usertodas las acciones del controlador que cambian las declaraciones de identidad actuales (ya que MVC solo hace esto para solicitudes posteriores, como se explica aquí ). Utilicé un filtro de resultados en lugar de middleware para agregar las cookies antiforgery a mis respuestas, lo que se aseguró de que solo se generaran después de que la acción MVC hubiera regresado.

Controlador (Nota: estoy administrando usuarios con ASP.NET Core Identity):

[Authorize]
[ValidateAntiForgeryToken]
public class AccountController : Controller
{
    private SignInManager<IdentityUser> signInManager;
    private UserManager<IdentityUser> userManager;
    private IUserClaimsPrincipalFactory<IdentityUser> userClaimsPrincipalFactory;

    public AccountController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory)
    {
        this.signInManager = signInManager;
        this.userManager = userManager;
        this.userClaimsPrincipalFactory = userClaimsPrincipalFactory;
    }

    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login(string username, string password)
    {
        if (username == null || password == null)
        {
            return BadRequest(); // Alias of 400 response
        }

        var result = await signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            var user = await userManager.FindByNameAsync(username);

            // Must manually set the HttpContext user claims to those of the logged
            // in user. Otherwise MVC will still include a XSRF token for the "null"
            // user and token validation will fail. (MVC appends the correct token for
            // all subsequent reponses but this isn't good enough for a single page
            // app.)
            var principal = await userClaimsPrincipalFactory.CreateAsync(user);
            HttpContext.User = principal;

            return Json(new { username = user.UserName });
        }
        else
        {
            return Unauthorized();
        }
    }

    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await signInManager.SignOutAsync();

        // Removing identity claims manually from the HttpContext (same reason
        // as why we add them manually in the "login" action).
        HttpContext.User = null;

        return Json(new { result = "success" });
    }
}

Filtro de resultados para agregar cookies antiforgery:

public class XSRFCookieFilter : IResultFilter
{
    IAntiforgery antiforgery;

    public XSRFCookieFilter(IAntiforgery antiforgery)
    {
        this.antiforgery = antiforgery;
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        var HttpContext = context.HttpContext;
        AntiforgeryTokenSet tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
        HttpContext.Response.Cookies.Append(
            "MyXSRFFieldTokenCookieName",
            tokenSet.RequestToken,
            new CookieOptions() {
                // Cookie needs to be accessible to Javascript so we
                // can append it to request headers in the browser
                HttpOnly = false
            } 
        );
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {

    }
}

Extracto de Startup.cs:

public partial class Startup
{
    public Startup(IHostingEnvironment env)
    {
        //...
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {

        //...

        services.AddAntiforgery(options =>
        {
            options.HeaderName = "MyXSRFFieldTokenHeaderName";
        });


        services.AddMvc(options =>
        {
            options.Filters.Add(typeof(XSRFCookieFilter));
        });

        services.AddScoped<XSRFCookieFilter>();

        //...
    }

    public void Configure(
        IApplicationBuilder app,
        IHostingEnvironment env,
        ILoggerFactory loggerFactory)
    {
        //...
    }
}
Ned Howley
fuente
-3

Tiene un problema con la validación de token antifalsificación en la tienda de Internet: los usuarios abren muchas pestañas (con productos) y después de iniciar sesión en una intenta iniciar sesión en otra y obtienen dicha AntiForgeryException. Entonces, AntiForgeryConfig.SuppressIdentityHeuristicChecks = true no me ayudó, así que usé un truco tan feo, tal vez sea útil para alguien:

   public class ExceptionPublisherExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext exceptionContext)
    {
        var exception = exceptionContext.Exception;

        var request = HttpContext.Current.Request;
        if (request != null)
        {
            if (exception is HttpAntiForgeryException &&
                exception.Message.ToLower().StartsWith("the provided anti-forgery token was meant for user \"\", but the current user is"))
            {
                var isAjaxCall = string.Equals("XMLHttpRequest", request.Headers["x-requested-with"], StringComparison.OrdinalIgnoreCase);
                var returnUrl = !string.IsNullOrWhiteSpace(request["returnUrl"]) ? request["returnUrl"] : "/";
                var response = HttpContext.Current.Response;

                if (isAjaxCall)
                {
                    response.Clear();
                    response.StatusCode = 200;
                    response.ContentType = "application/json; charset=utf-8";
                    response.Write(JsonConvert.SerializeObject(new { success = 1, returnUrl = returnUrl }));
                    response.End();
                }
                else
                {
                    response.StatusCode = 200;
                    response.Redirect(returnUrl);
                }
            }
        }


        ExceptionHandler.HandleException(exception);
    }
}

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new ExceptionPublisherExceptionFilter());
        filters.Add(new HandleErrorAttribute());
    }
}

Piense que sería genial si se pueden configurar las opciones de generación de tokens antifalsificación, para excluir el nombre de usuario o algo así.

usuario3364244
fuente
12
Este es un ejemplo terrible de tratar el problema en la pregunta. No uses esto.
xxbbcc
Totalmente de acuerdo con xxbbcc.
Javier
OK, caso de uso: formulario de inicio de sesión con token anti falsificación. Ábralo en 2 pestañas del navegador. Inicia sesión primero. No puedes actualizar la segunda pestaña. ¿Qué solución sugiere que tenga un comportamiento correcto para el usuario que intenta iniciar sesión desde la segunda pestaña?
user3364244
@ user3364244: el comportamiento correcto puede hacer lo siguiente: detectar un inicio de sesión externo utilizando websockets o signalR. Esta es la misma sesión, así que podrías hacer que funcione, supongo :-)
amortigua el