Por que AuthorizeAttribute redireciona para a página de logon para falhas de autenticação e autorização?

265

No ASP.NET MVC, você pode marcar um método de controlador com AuthorizeAttribute, assim:

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

Isso significa que, se o usuário conectado no momento não estiver na função "CanDeleteTags", o método do controlador nunca será chamado.

Infelizmente, para falhas, AuthorizeAttributeretorna HttpUnauthorizedResult, que sempre retorna o código de status HTTP 401. Isso causa um redirecionamento para a página de login.

Se o usuário não estiver logado, isso faz todo o sentido. No entanto, se o usuário estiver conectado, mas não estiver na função necessária, é confuso enviá-lo de volta à página de login.

Parece que AuthorizeAttributeconfunde autenticação e autorização.

Isso parece um pouco de supervisão no ASP.NET MVC, ou estou faltando alguma coisa?

Eu tive que preparar um DemandRoleAttributeque separa os dois. Quando o usuário não é autenticado, ele retorna HTTP 401, enviando-o para a página de login. Quando o usuário está conectado, mas não está na função necessária, ele cria um NotAuthorizedResult. Atualmente, isso é redirecionado para uma página de erro.

Certamente eu não precisava fazer isso?

Roger Lipscombe
fonte
10
Excelente pergunta e eu concordo, ele deve estar lançando um status HTTP não autorizado.
Pure.Krome
3
Eu gosto da sua solução, Roger. Mesmo se você não.
Jon Jon Davis
Minha página de login tem uma verificação para simplesmente redirecionar o usuário para o ReturnUrl, se ele já estiver autenticado automaticamente. Então, eu consegui criar um loop infinito de 302 redirecionamentos: D woot.
precisa saber é o seguinte
1
Confira isso .
Jogi 22/01
Roger, bom artigo sobre a sua solução - red-gate.com/simple-talk/dotnet/asp-net/... Parece que sua solução é a única maneira de fazer isso de forma limpa
Craig

Respostas:

305

Quando foi desenvolvido, o System.Web.Mvc.AuthorizeAttribute estava fazendo a coisa certa - as revisões mais antigas da especificação HTTP usavam o código de status 401 para "não autorizado" e "não autenticado".

A partir da especificação original:

Se a solicitação já incluía credenciais de autorização, a resposta 401 indica que a autorização foi recusada para essas credenciais.

De fato, você pode ver a confusão ali - ele usa a palavra "autorização" quando significa "autenticação". Na prática cotidiana, no entanto, faz mais sentido retornar um 403 Proibido quando o usuário é autenticado, mas não autorizado. É improvável que o usuário tenha um segundo conjunto de credenciais que lhes dê acesso - má experiência do usuário.

Considere a maioria dos sistemas operacionais - quando você tenta ler um arquivo que não tem permissão para acessar, não é exibida uma tela de login!

Felizmente, as especificações HTTP foram atualizadas (junho de 2014) para remover a ambiguidade.

Em "Protocolo de transporte de hipertexto (HTTP / 1.1): Autenticação" (RFC 7235):

O código de status 401 (não autorizado) indica que a solicitação não foi aplicada porque não possui credenciais de autenticação válidas para o recurso de destino.

Em "Protocolo de transferência de hipertexto (HTTP / 1.1): Semântica e conteúdo" (RFC 7231):

O código de status 403 (Proibido) indica que o servidor entendeu a solicitação, mas se recusa a autorizá-la.

Curiosamente, no momento em que o ASP.NET MVC 1 foi lançado, o comportamento do AuthorizeAttribute estava correto. Agora, o comportamento está incorreto - a especificação HTTP / 1.1 foi corrigida.

Em vez de tentar alterar os redirecionamentos da página de login do ASP.NET, é mais fácil corrigir o problema na fonte. Você pode criar um novo atributo com o mesmo nome ( AuthorizeAttribute) no espaço de nome padrão do seu site (isso é muito importante) e o compilador o buscará automaticamente em vez do padrão do MVC. Obviamente, você sempre pode atribuir um novo nome ao atributo se preferir usar essa abordagem.

[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
fonte
52
+1 Abordagem muito boa. Uma pequena sugestão: em vez de verificação filterContext.HttpContext.User.Identity.IsAuthenticated, você pode apenas verificar filterContext.HttpContext.Request.IsAuthenticated, que vem com verificações nulos construído em Sé. Stackoverflow.com/questions/1379566/...
Daniel Liuzzi
> Você pode criar um novo atributo com o mesmo nome (AuthorizeAttribute) no espaço para nome padrão do seu site, e o compilador o buscará automaticamente, em vez do padrão do MVC. Isso resulta em um erro: Não foi possível encontrar o tipo ou espaço de nome 'Autorizar' (está faltando uma diretiva ou uma referência de montagem?) Ambos using System.Web.Mvc; e o espaço para nome da minha classe AuthorizeAttribute personalizada é referenciada no controlador. Para resolver isso eu tive que usar [MyNamepace.Authorize]
stormwild
2
@ DePeter, a especificação nunca diz nada sobre um redirecionamento. Por que um redirecionamento é uma solução melhor? Isso por si só mata solicitações de ajax sem um hack para resolvê-lo.
Adam Tuliper - MSFT
1
Isso deve ser registrado no MS Connect porque é claramente um bug comportamental. Obrigado.
Tony Muro
BTW, por que somos redirecionados para a página de login? Por que não apenas gerar um código 401 e a página de login diretamente na mesma solicitação?
SandRock
25

Adicione isso à sua função Page_Load de login:

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

Quando o usuário é redirecionado para lá, mas já está logado, ele mostra a página não autorizada. Se eles não estiverem conectados, ele será exibido e exibirá a página de login.

Alan Jackson
fonte
18
Page_Load é um mojo de formulários da web
Chance
2
@ Chance - faça isso no ActionMethod padrão para o controlador que é chamado onde o FormsAuthencation foi configurado para chamar.
Pure.Krome
Isso realmente funciona muito bem, embora para MVC deva ser algo como if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");onde não autorizado é um nome de rota definido.
Moses Machua
Então você pergunta a um recurso, é redirecionado para uma página de login e é redirecionado novamente para uma página 403? Parece ruim para mim. Eu nem consigo tolerar um redirecionamento. IMO essa coisa é muito mal construída de qualquer maneira.
SandRock
3
De acordo com a sua solução, se você já fez login e vá para a página de Login digitando o URL ... isso o levará à página Não Autorizada. o que não está certo.
Rajshekar Reddy
4

Eu sempre pensei que isso fazia sentido. Se você estiver conectado e tentar acessar uma página que exija uma função que não possui, você será encaminhado para a tela de login solicitando que efetue login com um usuário que possui essa função.

Você pode adicionar lógica à página de login que verifica se o usuário já está autenticado. Você pode adicionar uma mensagem amigável que explica por que eles foram levados de volta para lá novamente.

Roubar
fonte
4
Sinto que a maioria das pessoas não costuma ter mais de uma identidade para um determinado aplicativo da web. Se o fizerem, são espertos o suficiente para pensar "meu ID atual não possui mojo, entrarei novamente como o outro".
Roger Lipscombe
Embora seu outro ponto sobre a exibição de algo na página de login seja bom. Obrigado.
Roger Lipscombe
4

Infelizmente, você está lidando com o comportamento padrão da autenticação de formulários do ASP.NET. Há uma solução alternativa (ainda não tentei) discutida aqui:

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

(Não é específico para o MVC)

Penso que, na maioria dos casos, a melhor solução é restringir o acesso a recursos não autorizados antes que o usuário tente chegar lá. Removendo / esmaecendo o link ou botão que pode levá-los a esta página não autorizada.

Provavelmente seria bom ter um parâmetro adicional no atributo para especificar para onde redirecionar um usuário não autorizado. Entretanto, olho para o AuthorizeAttribute como uma rede de segurança.

Keltex
fonte
Também planejo remover o link com base na autorização (vi uma pergunta aqui sobre isso em algum lugar), então codificarei um método de extensão HtmlHelper posteriormente.
Roger Lipscombe
1
Eu ainda tenho que impedir que o usuário vá diretamente para a URL, que é o objetivo desse atributo. Eu não estou muito feliz com o 401 solução Personalizado (parece um pouco global), então eu vou tentar modelar o meu NotAuthorizedResult em RedirectToRouteResult ...
Roger Lipscombe
0

Tente isso no manipulador Application_EndRequest do seu arquivo 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
fonte
0

Se você estiver usando o aspnetcore 2.0, use o seguinte:

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
fonte
0

No meu caso, o problema era "a especificação HTTP usava o código de status 401 para" não autorizado "e" não autenticado "". Como o ShadowChaser disse.

Esta solução funciona para mim:

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
fonte