Token zabezpieczający przed fałszerstwem jest przeznaczony dla użytkownika „”, ale aktualnym użytkownikiem jest „nazwa użytkownika”

132

Tworzę aplikację jednostronicową i mam problem z tokenami chroniącymi przed fałszerstwem.

Wiem, dlaczego tak się dzieje, ale nie wiem, jak go naprawić.

Pojawia się błąd, gdy dzieje się co następuje:

  1. Niezalogowany użytkownik ładuje okno dialogowe (z wygenerowanym tokenem zabezpieczającym przed fałszerstwem)
  2. Użytkownik zamyka okno dialogowe
  3. Użytkownik loguje się
  4. Użytkownik otwiera to samo okno dialogowe
  5. Użytkownik przesyła formularz w oknie dialogowym

Token zabezpieczający przed fałszerstwem jest przeznaczony dla użytkownika „”, ale aktualnym użytkownikiem jest „nazwa użytkownika”

Powodem tego jest to, że moja aplikacja jest w 100% jednostronicowa, a kiedy użytkownik loguje się pomyślnie za pośrednictwem posta w AJAX /Account/JsonLogin, po prostu przełączam bieżące widoki z „widokami uwierzytelnionymi” zwróconymi z serwera, ale nie ładuję ponownie strona.

Wiem, że to jest powód, ponieważ jeśli po prostu przeładuję stronę między krokami 3 i 4, nie ma błędu.

Wygląda więc na to, że @Html.AntiForgeryToken()w załadowanej formie nadal zwraca token dla starego użytkownika, dopóki strona nie zostanie ponownie załadowana.

Jak mogę zmienić, @Html.AntiForgeryToken()aby zwrócić token dla nowego, uwierzytelnionego użytkownika?

Wstrzykuję nowy GenericalPrincipalz niestandardowym IIdentityna co Application_AuthenticateRequesttak, zanim @Html.AntiForgeryToken()zostanie wywołany HttpContext.Current.User.Identity, w rzeczywistości moja niestandardowa tożsamość z IsAuthenticatedwłaściwością ustawioną na true, a mimo to @Html.AntiForgeryTokennadal wydaje się renderować token dla starego użytkownika, chyba że wykonam ponowne załadowanie strony.

parlament
źródło
Czy rzeczywiście możesz sprawdzić, czy kod @ Html.AntiForgeryToken jest wywoływany bez ponownego ładowania?
Kyle C
Zdecydowanie tak, mogę udać się tam, aby sprawdzić HttpContext.Current. Obiekt użytkownika, jak wspomniałem
parlament
2
Proszę odnieść się do tego: stackoverflow.com/a/19471680/193634
Rosdi Kasim
@parliament czy mógłbyś powiedzieć, na którą opcję wybrałeś w odpowiedzi poniżej.
Siddharth Pandey
Myślę, że zrobiłem wyjątek, aby przejść z pełnym przeładowaniem, jeśli dobrze pamiętam. Ale spodziewam się, że wkrótce napotkam ten problem w nowym projekcie. Opublikuję ponownie, jeśli wybiorę lepszą opcję pracy.
parlament

Odpowiedzi:

170

Dzieje się tak, ponieważ token chroniący przed fałszerstwem osadza nazwę użytkownika jako część zaszyfrowanego tokenu w celu lepszej weryfikacji. Przy pierwszym wywołaniu @Html.AntiForgeryToken()użytkownik nie jest zalogowany, więc token będzie miał pusty ciąg dla nazwy użytkownika, po zalogowaniu się użytkownika, jeśli nie zmienisz tokena przeciw fałszerstwu, nie przejdzie weryfikacji, ponieważ token początkowy był dla anonimowy użytkownik, a teraz mamy uwierzytelnionego użytkownika o znanej nazwie użytkownika.

Masz kilka możliwości rozwiązania tego problemu:

  1. Tylko tym razem pozwól swojemu SPA wykonać pełny POST, a po ponownym załadowaniu strony będzie miał token zapobiegający fałszerstwom z osadzoną zaktualizowaną nazwą użytkownika.

  2. Miej częściowy widok z samym @Html.AntiForgeryToken()i zaraz po zalogowaniu, wykonaj kolejne żądanie AJAX i zastąp istniejący token zabezpieczający przed fałszerstwem odpowiedzią na żądanie.

  3. Po prostu wyłącz sprawdzanie tożsamości, które przeprowadza weryfikacja przed fałszerstwem. Dodaj następującą do Application_Start metody: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true.

epignosisx
źródło
21
@parliament: zaakceptowałeś tę odpowiedź, czy mógłbyś podzielić się z nami wybraną opcją?
R. Schreurs
9
+1 za przyjemną i prostą opcję 3. Czasowe wylogowanie przez dostawców OAuth również powoduje ten problem.
Gone Coding
18
Opcja 3 nie zadziałała dla mnie. Po wylogowaniu otworzyłem dwa okna na stronie logowania. Zalogowano się jako jeden użytkownik w jednym oknie, a następnie zalogowałem się jako inny użytkownik w drugim i otrzymałem ten sam błąd.
McGaz
5
Niestety nie mogłem znaleźć dobrego rozwiązania tego problemu. Usunąłem token ze strony logowania. Nadal zamieszczam to w postach po zalogowaniu.
McGaz
8
Opcja 3 też nie zadziałała. Nadal pojawia się ten sam błąd.
Joao Leme
26

Aby naprawić błąd, musisz umieścić OutputCacheadnotację danych na stronie pobierania ActionResultlogowania jako:

[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
public ActionResult Login(string returnUrl)
user3401354
źródło
3
To rozwiązało problem dla mnie, ma sens. Dzięki!
Prime03
Mój przypadek użycia polegał na tym, że użytkownik próbował się zalogować i został wyświetlony błąd, np. „Konto wyłączone” za pośrednictwem ModelState.AddError (). Następnie, gdyby ponownie kliknęli przycisk logowania, zobaczyliby ten błąd. Jednak ta poprawka po prostu dała im ponownie pusty widok nowego logowania zamiast błędu tokena zapobiegającego fałszerstwom. Więc nie naprawiam.
yourpublicdisplayname
Mój przypadek: 1. Logowanie użytkownika () i lądowanie na stronie głównej. 2. Użytkownik naciska przycisk wstecz i wraca do widoku logowania. 3. Zaloguj się ponownie i zobacz błąd „Token ochrony przed fałszerstwem jest przeznaczony dla użytkownika” ”, ale bieżącym użytkownikiem jest„ nazwa użytkownika ”„ Na stronie błędu Jeśli użytkownik kliknie jakąkolwiek inną zakładkę z menu, aplikacja działała zgodnie z oczekiwaniami . Korzystając z powyższego kodu, użytkownik może nadal nacisnąć przycisk Wstecz, ale zostaje przekierowany na stronę główną. Bez względu na to, ile razy użytkownik kliknie przycisk Wstecz, przekieruje go na stronę główną. Dziękuję
Ravi
Jakieś pomysły, dlaczego to nie działa w widoku sieci Web platformy Xamarin?
Noobie3001
1
Aby uzyskać pełne wyjaśnienie, zobacz poprawę wydajności dzięki buforowaniu danych wyjściowych
stomy
15

Z moją aplikacją zdarza się to wiele razy, więc zdecydowałem się poszukać w Google!

Znalazłem proste wyjaśnienie tego błędu! Użytkownik dwukrotnie klika przycisk logowania! Możesz zobaczyć, jak inny użytkownik mówi o tym pod poniższym linkiem:

MVC 4 podał, że token zabezpieczający przed fałszerstwem był przeznaczony dla użytkownika „”, ale bieżący użytkownik to „użytkownik”

Mam nadzieję, że to pomoże! =)

Ricardo França
źródło
To był mój problem. Dzięki!
Nanou Ponette
10

Komunikat pojawia się po zalogowaniu, gdy jesteś już uwierzytelniony.

Ten pomocnik robi dokładnie to samo, co [ValidateAntiForgeryToken]atrybut.

System.Web.Helpers.AntiForgery.Validate()

Usuń [ValidateAntiForgeryToken]atrybut z kontrolera i umieść tego pomocnika w metodzie działania.

Jeśli więc użytkownik jest już uwierzytelniony, przekieruj na stronę główną, a jeśli nie, kontynuuj weryfikację ważnego tokena chroniącego przed fałszerstwem po tej weryfikacji.

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

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

Aby spróbować odtworzyć błąd, wykonaj następujące czynności: Jeśli jesteś na swojej stronie logowania i nie jesteś uwierzytelniony. Jeśli zduplikujesz kartę i zalogujesz się na drugiej karcie. A jeśli wrócisz do pierwszej zakładki na stronie logowania i spróbujesz się zalogować bez przeładowywania strony ... masz ten błąd.

A. Morel
źródło
Doskonałe rozwiązanie! To rozwiązało mój problem po wypróbowaniu wielu innych sugestii, które nie działały. Po pierwsze, był to ból odtwarzający błąd, dopóki nie odkryłem, że może to być spowodowane tym, że dwie przeglądarki lub karty są otwarte z tą samą stroną, a użytkownik loguje się z jednej, a następnie loguje się z drugiej bez ponownego ładowania.
Nicki
Dzięki za to rozwiązanie. Dla mnie też zadziałał. Dodałem sprawdzenie, czy tożsamość jest taka sama jak nazwa użytkownika logowania, a jeśli tak, z radością kontynuuję próbę zalogowania użytkownika i wylogowania go, jeśli tak nie jest. Na przykład spróbuj {System.Web.Helpers.AntiForgery.Validate ();} catch (HttpAntiForgeryException) {if (! User.Identity.IsAuthenticated || string.Compare (User.Identity.Name, model.Username)! = 0) {// Tutaj logika wylogowania}}
Steve Owen
Wielkie dzięki! szczególnie pomocny był przewodnik, jak odtworzyć błąd!
Tim Gerhard
9

Miałem ten sam problem i ten brudny hack go naprawił, przynajmniej do czasu, gdy mogę to naprawić w czystszy sposób.

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

...

mnemonika
źródło
2
Wygląda na to, że miałem ten sam problem. IMO to nie włamanie, to bardziej powszechna rzecz, którą wszyscy powinniśmy sprawdzać przy logowaniu. Jeśli użytkownik jest już zalogowany, po prostu wyloguj go i wyświetl stronę logowania. Naprawiłem mój problem, dziękuję.
Alexandre
2

Mam ten sam wyjątek, który występuje przez większość czasu na serwerze produkcyjnym.

Dlaczego tak się dzieje?

Dzieje się tak, gdy użytkownik loguje się z ważnymi poświadczeniami i po zalogowaniu się i przekierowaniu na inną stronę, a po naciśnięciu przycisku Wstecz wyświetli stronę logowania i ponownie wprowadzi prawidłowe poświadczenia, kiedy wystąpi ten wyjątek.

Jak rozwiązać?

Po prostu dodaj tę linię i działaj idealnie, bez błędu.

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")]
user8478
źródło
1

Miałem dość specyficzny, ale podobny problem w procesie rejestracji. Po kliknięciu przez użytkownika odsyłacza e-mail wysłanego do niego zostanie zalogowany i wysłany bezpośrednio do ekranu szczegółów konta, aby podać więcej informacji. Mój kod to:

    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

Okazało się, że widok zwrotu („AccountDetails”) dawał mi wyjątek dotyczący tokena, zgaduję, ponieważ funkcja ConfirmEmail została ozdobiona wartością AllowAnonymous, ale funkcja AccountDetails miała ValidateAntiForgeryToken.

Zmiana opcji Return to Return RedirectToAction („AccountDetails”) rozwiązała problem za mnie.

Liam
źródło
1
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]

public ActionResult Login(string returnUrl)

Możesz to sprawdzić, umieszczając punkt przerwania w pierwszym wierszu akcji Zaloguj (Pobierz). Przed dodaniem dyrektywy OutputCache punkt przerwania zostałby osiągnięty przy pierwszym załadowaniu, ale po kliknięciu przycisku wstecz przeglądarki nie. Po dodaniu dyrektywy powinieneś zakończyć za każdym razem trafienie w punkt przerwania, więc AntiForgeryToken będzie tym poprawnym, a nie pustym.

Marian Dalalau
źródło
0

Miałem ten sam problem z jednostronicową aplikacją ASP.NET MVC Core. Rozwiązałem to, ustawiając HttpContext.Userwe wszystkich akcjach kontrolera, które zmieniają bieżące oświadczenia o tożsamości (ponieważ MVC robi to tylko dla kolejnych żądań, jak omówiono tutaj ). Użyłem filtru wyników zamiast oprogramowania pośredniego, aby dołączyć pliki cookie zapobiegające fałszerstwom do moich odpowiedzi, co upewniło się, że zostały wygenerowane dopiero po powrocie akcji MVC.

Kontroler (NB. Zarządzam użytkownikami za pomocą 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" });
    }
}

Filtr wyników umożliwiający dołączenie plików cookie zapobiegających fałszerstwom:

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)
    {

    }
}

Ekstrakt 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
źródło
-3

Ma problem z walidacją anty-fałszerstwa w sklepie internetowym: użytkownicy otwierają wiele zakładek (z towarem) i po zalogowaniu się na jednej próbują zalogować się na innym i dostaje taki AntiForgeryException. Więc AntiForgeryConfig.SuppressIdentityHeuristicChecks = true mi nie pomogło, więc użyłem takiego brzydkiego hackfixa, może komuś się przyda:

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

Pomyśl, że będzie wspaniale, jeśli można ustawić opcje generowania tokenów przed fałszerstwem, aby wykluczyć nazwę użytkownika lub coś w tym rodzaju.

user3364244
źródło
12
To straszny przykład rozwiązania problemu w pytaniu. Nie używaj tego.
xxbbcc,
Całkowicie zgadzam się z xxbbcc.
Javier
OK, przypadek użycia: formularz logowania z tokenem chroniącym przed fałszerstwem. Otwórz go w 2 kartach przeglądarki. Zaloguj się najpierw. Ty cant odświeżyć drugą zakładkę. Jakie rozwiązanie sugerujesz, aby zachować poprawne zachowanie dla użytkownika próbującego zalogować się z drugiej zakładki?
user3364244
@ user3364244: poprawne zachowanie to: wykrycie zewnętrznego logowania przy użyciu websockets lub signalR. To jest ta sama sesja, więc myślę, że możesz sprawić, że zadziała :-)
dampee