O token anti-falsificação destina-se ao usuário "", mas o usuário atual é "nome de usuário"

130

Estou criando um aplicativo de página única e tendo um problema com os tokens anti-falsificação.

Sei por que o problema acontece, simplesmente não sei como corrigi-lo.

Recebo o erro quando acontece o seguinte:

  1. O usuário não conectado carrega uma caixa de diálogo (com um token antifalsificação gerado)
  2. Usuário fecha a caixa de diálogo
  3. Usuário faz login
  4. Usuário abre a mesma caixa de diálogo
  5. Usuário envia formulário na caixa de diálogo

O token anti-falsificação destina-se ao usuário "", mas o usuário atual é "nome de usuário"

A razão pela qual isso acontece é que meu aplicativo é 100% de página única e, quando um usuário efetua login com êxito em uma postagem do ajax /Account/JsonLogin, simplesmente troco as visualizações atuais com as "visualizações autenticadas" retornadas do servidor, mas não recarrego o página.

Sei que esse é o motivo, porque se eu simplesmente recarregar a página entre as etapas 3 e 4, não haverá erro.

Portanto, parece que @Html.AntiForgeryToken()no formulário carregado ainda retorna um token para o usuário antigo até que a página seja recarregada.

Como posso alterar @Html.AntiForgeryToken()para retornar um token para o novo usuário autenticado?

Eu injeto um novo GenericalPrincipalcom um costume IIdentityde cada Application_AuthenticateRequestvez que @Html.AntiForgeryToken()é chamado HttpContext.Current.User.Identity, na verdade, minha identidade personalizada com IsAuthenticatedpropriedade definida como true e ainda @Html.AntiForgeryTokenassim parece render um token para o usuário antigo, a menos que eu recarregue a página.

parlamento
fonte
Você pode realmente verificar se o código @ Html.AntiForgeryToken está sendo chamado sem recarregar?
Kyle C
Ele definitivamente é, posso pausa bem sucedido lá para inspecionar HttpContext.Current.User objeto como eu mencionei
o parlamento
2
Consulte este: stackoverflow.com/a/19471680/193634
Rosdi Kasim
@ parlamentar, por favor, diga qual opção você escolheu na resposta abaixo.
Siddharth Pandey
Eu acredito que fiz uma exceção para ir com uma recarga completa, se bem me lembro. Mas espero encontrar esse problema muito em breve em um novo projeto. Postarei de volta se eu optar por uma opção de trabalho melhor.
parliament

Respostas:

170

Isso está acontecendo porque o token antifalsificação incorpora o nome de usuário do usuário como parte do token criptografado para melhor validação. Quando você chama pela primeira vez @Html.AntiForgeryToken()o usuário não está conectado, o token terá uma sequência vazia para o nome de usuário. Depois que o usuário efetuar login, se você não substituir o token antifalsificação, ele não passará na validação porque o token inicial era para usuário anônimo e agora temos um usuário autenticado com um nome de usuário conhecido.

Você tem algumas opções para resolver esse problema:

  1. Desta vez, deixe seu SPA fazer um POST completo e, quando a página for recarregada, ele terá um token antifalsificação com o nome de usuário atualizado incorporado.

  2. Tenha uma visão parcial @Html.AntiForgeryToken()logo após o login, faça outra solicitação AJAX e substitua seu token antifalsificação existente pela resposta da solicitação.

  3. Apenas desative a verificação de identidade que a validação antifalsificação executa. Adicione o seguinte ao seu Application_Start método: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.

epignosisx
fonte
21
@ parlamentar: você aceitou esta resposta, poderia compartilhar conosco qual opção você escolheu?
R. Schreurs
9
+1 para a opção agradável e simples 3. Os logouts temporizados dos provedores OAuth também causam esse problema.
Gone Coding
18
A opção 3 não funcionou para mim. Enquanto desconectado, abri duas janelas na página de login. Efetuou login como um usuário em uma janela e, em seguida, logon como outro usuário na outra e recebeu o mesmo erro.
McGaz
5
Infelizmente, não consegui uma boa solução para isso. Eu removi o token da página de login. Ainda o incluo nas postagens após o login.
McGaz
7
A opção 3 também não funcionou para mim. Ainda está recebendo o mesmo erro.
Joao Leme
25

Para corrigir o erro, você precisa colocar a OutputCacheanotação de dados na página Obter ActionResultlogon como:

[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
public ActionResult Login(string returnUrl)
user3401354
fonte
3
Isso resolveu o problema para mim, faz total sentido. Obrigado!
precisa saber é o seguinte
Meu caso de uso foi o usuário que tentou um logon e recebeu um erro, por exemplo, "conta desativada" via ModelState.AddError (). Então, se eles clicassem no login novamente, eles veriam esse erro. No entanto, essa correção deu a eles uma nova visão de login em branco novamente, em vez do erro do token anti-falsificação. Então, não é uma correção.
Yourpublicdisplayname
Meu caso: 1. O usuário efetua login () e chega à página inicial. 2. O usuário pressiona o botão Voltar e volta para a visualização Login. 3. Faça login novamente e veja o erro "O token anti-falsificação destina-se ao usuário" ", mas o usuário atual é" nome de usuário "" Na página de erro Se o usuário clicar em qualquer outra guia do menu, o aplicativo estava funcionando conforme o esperado . Usando o código acima, o usuário ainda pode pressionar o botão Voltar, mas ele é redirecionado para a página inicial. Portanto, não importa quantas vezes o usuário pressione o botão Voltar, ele será redirecionado para a página inicial. Obrigado
Ravi
Alguma idéia de por que isso não funciona em uma visualização na web do Xamarin?
Noobie3001 13/05/19
1
Para uma explicação completa ver melhorar o desempenho com cache de saída
stomy
8

Eu tinha o mesmo problema, e esse truque sujo o corrigiu, pelo menos até que eu possa consertá-lo de uma maneira mais limpa.

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

...

mnemônicos
fonte
1
Parece que eu tive o mesmo problema. OMI não é um hack, é uma coisa mais comum que todos devemos procurar ao fazer login. Se o usuário já estiver conectado, basta desconectá-lo e exibir a página de login. Corrigido o meu problema, obrigado.
Alexandre
7

A mensagem aparece quando você faz o login quando já está autenticado.

Este auxiliar faz exatamente a mesma coisa que [ValidateAntiForgeryToken]atributo.

System.Web.Helpers.AntiForgery.Validate()

Remova a [ValidateAntiForgeryToken]atribuição do controlador e coloque esse auxiliar no método de ação.

Portanto, quando o usuário já estiver autenticado, redirecione para a página inicial ou, caso contrário, continue com a verificação do token anti-falsificação válido após essa verificação.

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

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

Para tentar reproduzir o erro, proceda da seguinte maneira: Se você estiver na sua página de login e não estiver autenticado. Se você duplicar a guia e fizer login com a segunda guia. E se você voltar para a primeira guia na página de login e tentar fazer login sem recarregar a página ... você tem esse erro.

A. Morel
fonte
Excelente solução! Isso resolveu meu problema depois de tentar muitas outras sugestões que não funcionaram. Primeiro, foi difícil reproduzir o erro, até que descobri que poderia ser por causa de dois navegadores ou guias abertos com a mesma página, e o usuário efetuando login em um e depois efetuando login na segunda sem recarregar.
Nicki
Obrigado por esta solução. Trabalhou para mim também. Eu adicionei uma verificação para ver se a identidade era a mesma do nome de usuário de login e, se assim for, continuo tentando entrar e logar com o usuário, caso contrário não. Por exemplo, tente {System.Web.Helpers.AntiForgery.Validate ();} catch (HttpAntiForgeryException) {if (! User.Identity.IsAuthenticated || string.Compare (User.Identity.Name, model.Username)! = 0) {// Sua lógica de logoff aqui}}
Steve Owen
2

Eu tenho a mesma exceção que ocorre na maioria das vezes no servidor de produção.

Por que isso acontece?

Isso acontece quando o usuário faz login com credenciais válidas e, uma vez logado e redirecionado para outra página, e depois que eles pressionam o botão Voltar, a página de login é exibida e, novamente, ele inseriu credenciais válidas no momento em que essa exceção ocorrerá.

Como resolver?

Basta adicionar esta linha e funcionar perfeitamente, sem erros.

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

Eu tive um problema bastante específico, mas similar, dentro do processo de registro. Depois que o usuário clica no link de e-mail enviado a ele, ele é conectado e enviado diretamente para a tela de detalhes da conta para preencher mais algumas informações. Meu código era:

    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

Descobri que a exibição de retorno ("AccountDetails") estava me dando a exceção do token, acho que porque a função ConfirmEmail foi decorada com AllowAnonymous, mas a função AccountDetails tinha ValidateAntiForgeryToken.

Alterar o retorno para retornar RedirectToAction ("AccountDetails") resolveu o problema para mim.

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

public ActionResult Login(string returnUrl)

Você pode testar isso colocando um ponto de interrupção na primeira linha da sua ação de Login (Obter). Antes de adicionar a diretiva OutputCache, o ponto de interrupção seria atingido na primeira carga, mas depois de clicar no botão voltar do navegador, não seria. Depois de adicionar a diretiva, você deve acabar com o ponto de interrupção sempre, para que o AntiForgeryToken seja o correto, e não o vazio.

Marian Dalalau
fonte
0

Eu tive o mesmo problema com um aplicativo ASP.NET MVC Core de página única. Eu o resolvi definindo HttpContext.Userem todas as ações do controlador que alteram as declarações de identidade atuais (já que o MVC faz isso apenas para solicitações subsequentes, conforme discutido aqui ). Usei um filtro de resultados em vez do middleware para anexar os cookies antiforgery às minhas respostas, o que garantiu que eles fossem gerados somente após o retorno da ação MVC.

Controller (NB. Estou gerenciando usuários com o 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 anexar 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)
    {

    }
}

Extrato 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
fonte
-3

Tem um problema com a validação de token anti-falsificação na loja virtual: os usuários abrem muitas guias (com mercadorias) e, depois de fazer logon em uma, tentam fazer logon em outra e obtêm essa AntiForgeryException. Portanto, AntiForgeryConfig.SuppressIdentityHeuristicChecks = true não me ajudou, então eu usei esse hackfix feio, talvez seja útil para alguém:

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

Acho que será ótimo se as opções de geração de token anti-falsificação puderem ser definidas, para excluir nome de usuário ou algo assim.

user3364244
fonte
12
Este é um exemplo terrível de como lidar com o problema na questão. Não use isso.
Xxbbcc # 4/14
Concordo totalmente com xxbbcc.
Javier
OK, use case: formulário de login com token anti-falsificação. Abra-o em 2 abas do navegador. Faça o login primeiro. Você não pode atualizar a segunda guia. Que solução você sugere que tenha um comportamento correto para o usuário que tenta fazer login na segunda guia?
user3364244
@ user3364244: o comportamento correto pode: detectar um logon externo usando websockets ou signalR. Esta é a mesma sessão assim que você poderia fazê-lo funcionar, eu acho :-)
dampee