Dlaczego AuthorizeAttribute przekierowuje na stronę logowania w przypadku błędów uwierzytelnienia i autoryzacji?

265

W ASP.NET MVC możesz oznaczyć metodę kontrolera za pomocą AuthorizeAttribute:

[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

Oznacza to, że jeśli aktualnie zalogowany użytkownik nie ma roli „CanDeleteTags”, metoda kontrolera nigdy nie zostanie wywołana.

Niestety w przypadku awarii AuthorizeAttributezwraca HttpUnauthorizedResult, który zawsze zwraca kod stanu HTTP 401. Powoduje to przekierowanie na stronę logowania.

Jeśli użytkownik nie jest zalogowany, ma to sens. Jeśli jednak użytkownik jest już zalogowany, ale nie ma wymaganej roli, mylące jest wysyłanie go z powrotem na stronę logowania.

Wygląda na to, że AuthorizeAttributełączy uwierzytelnianie i autoryzację.

To wydaje się trochę przeoczone w ASP.NET MVC, czy coś mi brakuje?

Musiałem przygotować coś, DemandRoleAttributeco oddziela te dwa elementy. Gdy użytkownik nie jest uwierzytelniony, zwraca HTTP 401, wysyłając go na stronę logowania. Gdy użytkownik jest zalogowany, ale nie ma wymaganej roli, tworzy NotAuthorizedResultzamiast niego. Obecnie przekierowuje to na stronę błędu.

Z pewnością nie musiałem tego robić?

Roger Lipscombe
źródło
10
Doskonałe pytanie i zgadzam się, że powinienem nadać status HTTP Not Authorized.
Pure.Krome
3
Podoba mi się twoje rozwiązanie, Roger. Nawet jeśli nie.
Jon Davis
Moja strona logowania ma opcję przekierowania użytkownika do ReturnUrl, jeśli jest on już uwierzytelniony. Udało mi się stworzyć nieskończoną pętlę 302 przekierowań: D woot.
juhan_h
1
Sprawdź to .
Jogi
Roger, dobry artykuł na temat twojego rozwiązania - red-gate.com/simple-talk/dotnet/asp-net/… Wygląda na to, że twoje rozwiązanie jest jedynym sposobem na zrobienie tego czysto
Craig

Odpowiedzi:

305

Gdy został opracowany po raz pierwszy, System.Web.Mvc.AuthorizeAttribute działał dobrze - starsze wersje specyfikacji HTTP używały kodu stanu 401 zarówno dla „nieautoryzowanego”, jak i „nieuwierzytelnionego”.

Z oryginalnej specyfikacji:

Jeśli żądanie zawierało już poświadczenia autoryzacji, wówczas odpowiedź 401 wskazuje, że odmówiono autoryzacji tych poświadczeń.

W rzeczywistości widać zamieszanie - używa słowa „autoryzacja”, gdy oznacza „uwierzytelnianie”. Jednak w codziennej praktyce bardziej sensowne jest zwracanie 403 Zabronione, gdy użytkownik jest uwierzytelniony, ale nie autoryzowany. Jest mało prawdopodobne, że użytkownik będzie miał drugi zestaw poświadczeń, które dadzą mu dostęp - złe wrażenia użytkownika dookoła.

Zastanów się nad większością systemów operacyjnych - gdy próbujesz odczytać plik, do którego nie masz uprawnień, nie wyświetla się ekran logowania!

Na szczęście specyfikacje HTTP zostały zaktualizowane (czerwiec 2014 r.) W celu usunięcia niejasności.

Z „Hyper Text Transport Protocol (HTTP / 1.1): Uwierzytelnianie” (RFC 7235):

Kod stanu 401 (nieautoryzowany) wskazuje, że żądanie nie zostało zastosowane, ponieważ brakuje prawidłowych poświadczeń uwierzytelnienia dla zasobu docelowego.

Z „Hypertext Transfer Protocol (HTTP / 1.1): Semantyka i treść” (RFC 7231):

Kod statusu 403 (Zabroniony) wskazuje, że serwer zrozumiał żądanie, ale odmawia jego autoryzacji.

Co ciekawe, w momencie wydania programu ASP.NET MVC 1 zachowanie funkcji AuthorizeAttribute było prawidłowe. Teraz zachowanie jest nieprawidłowe - poprawiono specyfikację HTTP / 1.1.

Zamiast próby zmiany przekierowań strony logowania ASP.NET, łatwiej jest po prostu naprawić problem u źródła. Możesz utworzyć nowy atrybut o tej samej nazwie ( AuthorizeAttribute) w domyślnej przestrzeni nazw swojej witryny (jest to bardzo ważne), a następnie kompilator automatycznie wybierze ją zamiast standardowej MVC. Oczywiście zawsze możesz nadać atrybutowi nową nazwę, jeśli wolisz takie podejście.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}
ShadowChaser
źródło
52
+1 Bardzo dobre podejście. Mała sugestia: zamiast sprawdzania filterContext.HttpContext.User.Identity.IsAuthenticated, możesz po prostu sprawdzić filterContext.HttpContext.Request.IsAuthenticated, która zawiera wbudowane czeki zerowe. Zobacz stackoverflow.com/questions/1379566/…
Daniel Liuzzi
> Możesz utworzyć nowy atrybut o tej samej nazwie (AuthorizeAttribute) w domyślnym obszarze nazw swojej witryny, a następnie kompilator automatycznie go wybierze zamiast standardowego MVC. Powoduje to błąd: Nie można znaleźć typu lub przestrzeni nazw „Autoryzuj” (brakuje dyrektywy lub odwołania do zestawu?) Oba używają System.Web.Mvc; a przestrzeń nazw dla mojej niestandardowej klasy AuthorizeAttribute jest przywoływana w kontrolerze. Aby rozwiązać ten problem, musiałem użyć [MyNamepace.Authorize]
stormwild 13.11.11
2
@ DePeter specyfikacja nigdy nie mówi nic o przekierowaniu, więc dlaczego przekierowanie jest lepszym rozwiązaniem? To samo zabija żądania ajax bez włamania, aby je rozwiązać.
Adam Tuliper - MSFT,
1
To powinno być zalogowane na MS Connect, ponieważ jest to wyraźnie błąd behawioralny. Dzięki.
Tony Wall
BTW, dlaczego my przekierowywani na stronę logowania? Dlaczego po prostu nie wypisać kodu 401 i strony logowania bezpośrednio w ramach tego samego żądania?
SandRock
25

Dodaj to do swojej funkcji logowania Page_Load:

// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

Gdy użytkownik zostaje tam przekierowany, ale jest już zalogowany, wyświetla nieautoryzowaną stronę. Jeśli nie są zalogowani, przechodzi przez i wyświetla stronę logowania.

Alan Jackson
źródło
18
Page_Load to webforms mojo
Szansa
2
@Chance - następnie zrób to w domyślnej ActionMethod dla kontrolera, który jest wywoływany tam, gdzie ma być wywoływane FormsAuthencation.
Pure.Krome
To naprawdę działa naprawdę dobrze, chociaż dla MVC powinno to być coś w rodzaju, w if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");którym Nieautoryzowane to zdefiniowana nazwa trasy.
Mojżesz Machua,
Więc pytasz o zasób, zostajesz przekierowany na stronę logowania i ponownie przekierowany na stronę 403? Wydaje mi się źle. W ogóle nie mogę tolerować jednego przekierowania. IMO to i tak jest bardzo źle zbudowane.
SandRock
3
Zgodnie z rozwiązaniem, jeśli jesteś już zalogowany i przejdź do strony logowania, wpisując adres URL ... spowoduje to przejście do strony Nieautoryzowane. co jest nie tak.
Rajshekar Reddy,
4

Zawsze myślałem, że to ma sens. Jeśli jesteś zalogowany i próbujesz wejść na stronę, która wymaga roli, której nie masz, zostaniesz przekierowany do ekranu logowania z prośbą o zalogowanie się z użytkownikiem, który ma tę rolę.

Możesz dodać logikę do strony logowania, która sprawdza, czy użytkownik jest już uwierzytelniony. Możesz dodać przyjazną wiadomość, która wyjaśnia, dlaczego zostali tam ponownie sparaliżowani.

Obrabować
źródło
4
Mam wrażenie, że większość ludzi nie ma więcej niż jednej tożsamości dla danej aplikacji internetowej. Jeśli tak, to są na tyle sprytni, by myśleć: „mój obecny identyfikator nie ma mojo, zaloguję się jako drugi”.
Roger Lipscombe
Chociaż drugi punkt dotyczący wyświetlania czegoś na stronie logowania jest dobry. Dzięki.
Roger Lipscombe
4

Niestety masz do czynienia z domyślnym zachowaniem uwierzytelniania formularzy ASP.NET. Jest obejście (nie próbowałem tego) omówione tutaj:

http://www.codeproject.com/KB/aspnet/Custon401Page.aspx

(To nie jest specyficzne dla MVC)

Myślę, że w większości przypadków najlepszym rozwiązaniem jest ograniczenie dostępu do nieautoryzowanych zasobów, zanim użytkownik spróbuje się tam dostać. Przez usunięcie / wyszarzenie linku lub przycisku, który może doprowadzić ich do tej nieautoryzowanej strony.

Prawdopodobnie byłoby miło mieć dodatkowy parametr w atrybucie, aby określić, gdzie przekierować nieautoryzowanego użytkownika. Ale w międzyczasie patrzę na AuthorizeAttribute jako sieć bezpieczeństwa.

Keltex
źródło
Planuję również usunąć link na podstawie autoryzacji (gdzieś tu o to pytałem), więc w przyszłości opiszę metodę rozszerzenia HtmlHelper.
Roger Lipscombe
1
Nadal muszę uniemożliwić użytkownikowi przejście bezpośrednio do adresu URL, na tym właśnie polega ten atrybut. Nie jestem zbyt zadowolony z rozwiązania Custom 401 (wydaje się nieco globalne), więc spróbuję modelować mój NotAuthorizedResult na RedirectToRouteResult ...
Roger Lipscombe
0

Wypróbuj to w module obsługi Application_EndRequest pliku Global.ascx

if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}
Kareem Cambridge
źródło
0

Jeśli używasz aspnetcore 2.0, użyj tego:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}
Greg Gum
źródło
0

W moim przypadku problemem był „specyfikacja HTTP użyła kodu stanu 401 zarówno dla„ nieautoryzowanego ”, jak i„ nieuwierzytelnionego ”. Jak powiedział ShadowChaser.

To rozwiązanie działa dla mnie:

if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}
César León
źródło