ASP.NET Core MVC - Role based Authorization


  Hoje vamos recordar como usar a autorização baseada em perfis do usuário na ASP .NET Core.

Quando uma identidade é criada, ela pode pertencer a uma ou mais roles ou perfis.

Assim um usuário poder pertencer aos perfis/roles de administrador e usuário, enquanto outro pode pertencer apenas ao perfil ou role usuário. A forma como essas roles são criadas e gerenciadas depende do armazenamento de e do processo de autorização. As roles são expostas ao desenvolvedor por meio do método IsInRole na classe ClaimsPrincipal.

Embora as roles sejam claims ou declarações, nem todas as declarações são roles. Dependendo do emissor de identidade, uma role pode ser uma coleção de usuários que podem aplicar claims(reivindicações) para membros do grupo, bem como uma claim ou reivindicação real sobre uma identidade.

No entanto, as claims devem ser informações sobre um usuário individual. O uso de roles para adicionar claims a um usuário pode confundir o limite entre o usuário e suas declarações individuais. Essa confusão é o motivo pelo qual os modelos SPA não são projetados com base em roles.

Além disso, para organizações que migram de um sistema legado local, a proliferação de roles ao longo dos anos pode significar que uma claim de role pode ser muito grande para ser contida em um token utilizável por SPAs.

As verificações de autorização baseadas em role são declarativas, o programador pode incorporá-las em seu código, contra um controlador ou Action. Isso especifica as funções de usuário que o usuário atual está autorizado a acessar o recurso solicitado.

A ASP.NET Core Identity é um programa de associação, que permite ao usuário adicionar funcionalidade de autenticação e autorização às aplicações web; assim, o usuário pode acessar o sistema em nome de suas roles/claims ou perfis/reivindicações onde usando a autorização, cada usuário autenticado tem acesso limitado ao aplicativo da web.

Por exemplo, o código a seguir limita o acesso do usuário no nível do controlador, significa que os usuários com função Admin têm direitos para acessar este controlador.

[Authorize(Roles ="Admin")]
public class HomeController : Controlador
{

}

Você também pode especificar várias roles como uma lista separada por vírgulas.

[Authorize(Roles ="Inventario,Financas")]
public class HomeController : Controlador
{
}

Este controlador seria acessível apenas para usuários com função de Inventário ou função de Finanças.

Você também pode aplicar vários atributos, então um usuário que acessa deve ser um membro de todas as funções especificadas.

No exemplo de código a seguir, o usuário deve ser membro das funções de Inventário e Finanças :

[Authorize(Roles = "Inventário")]
[Authorize(Roles = "Finanças")]
public class HomeController : Controlador
{
}

Você também pode limitar o usuário aplicando o atributo de autorização de função no nível de ação.

[Authorize(Roles ="Admin,User")]
public IActionResult Index()
{
   return View();
}

Este método Action é acessível para usuários com função Admin ou Usuário.

A seguir vamos criar uma aplicação ASP .NET Core MVC usando o VS 2022 Community e o .NET 7.0 para mostrar um exemplo prático e simples onde vamos definir os perfis usando Claims.

Criando o projeto MVC

Abra o VS 2022 e selecione o template ASP.NET Core Web App e informe o nome MvcRoleBased definindo as seguintes configurações para o projeto:

Na pasta Models do projeto vamos criar a view model LoginViewModel que iremos usar para definir a lógica da view Login:

using System.ComponentModel.DataAnnotations;
namespace MvcRoleBased.Models;
public class LoginViewModel
{
    [Required(ErrorMessage = "Informe o nome")]
    [Display(Name = "Usuário")]

    public string? UserName { get; set; }
    [Required(ErrorMessage = "Informe a senha")]
    [DataType(DataType.Password)]
    [Display(Name = "Senha")]
    public string? Password { get; set; }
    public string? ReturnUrl { get; set; }
}

Apenas para recordar, a ViewModel é uma classe que contém os campos que são representados na exibição fortemente tipada. Ele é usado para passar dados do controlador para a exibição fortemente tipada. Assim uma :

Usando uma view model estamos desacoplando o nosso modelo de domínio da View.

A seguir vamos adicionar o serviço de autenticação e definir algumas opções para o cookie de autenticação. Podemos definir o caminho para a página de login; e para a página de acesso negado (posteriormente adicionaremos ambas as páginas nesses caminhos), e p demos também definir o período de expiração do cookie.

Também devemos adicionar o middleware de autenticação e autorização ao pipeline de solicitação. Eles podem ser adicionados com as funções UseAuthentication e UseAuthorization. A ordem dessas chamadas é importante - a autenticação deve preceder a autorização.

Para isso inclua o código a seguir na classe Program:

using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services
  .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddCookie(options=>
  {
    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/AccessDenied";
    // previne que o cookie seja acessado
    // via javascript no cliente
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(3);
    options.SlidingExpiration = true;
  });
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

O atributo SameSite é um padrão de rascunho do IETF projetado para fornecer alguma proteção contra ataques de falsificação de solicitação entre sites (CSRF). O valor SameSiteMode.None deve ser usado para permitir o uso de cookies entre sites.

O AuthenticationScheme passado para AddAuthentication define o esquema de autenticação padrão para o aplicativo. O AuthenticationScheme é útil quando há várias instâncias de autenticação de cookie e você deseja autorizar com um esquema específico.

O método UseCookiePolicy() adiciona o manipulador CookiePolicyMiddleware ao IApplicationBuilder especificado, que habilita os recursos de política de cookies.

Estamos usando também AddHttpCOntextAccessor que adiciona uma implementação padrão para o serviço da interface IHttpContextAccessor.

Essa interface nos permite acessar a propriedade HttpContext que, por sua vez, fornece acesso à coleção Request e também à propriedade Response.

Vamos usar este serviço para obter informações do usuário e do perfil do usuário na aplicação.

A próxima tarefa é ajustar o código do controlador HomeController conforme mostrado a seguir:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MvcRoleBased.Models;
using System.Diagnostics;
using System.Security.Claims;
namespace MvcRoleBased.Controllers;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IHttpContextAccessor _httpContextAccessor;
    public HomeController(ILogger<HomeController> logger, 
                         IHttpContextAccessor httpContextAccessor)
    {
        _logger = logger;
        _httpContextAccessor = httpContextAccessor;
    }
    public IActionResult Index()
    {
        var user = _httpContextAccessor.HttpContext?
                                       .User
                                       .FindFirstValue(ClaimTypes.Name);
        if (user == null)
            return RedirectToAction("Login", "Account");
        return View();
    }
    [Authorize(Roles = "Admin")]
    public IActionResult Segredo()
    {
        var user = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Name);
        var role = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role);
        ViewBag.UserName = user;
        ViewBag.Role = role;
        return View();
    }
    [Authorize(Roles = "User")]
    public IActionResult Privacy()
    {
        var user = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Name);
        var role = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role);
        ViewBag.UserName = user;
        ViewBag.Role = role;
        return View();
    }
    [Authorize(Roles = "Admin,User")]
    public IActionResult AcessoUser()
    {
        var user = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Name);
        var role = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role);
        ViewBag.UserName = user;
        ViewBag.Role = role;
        return View();
    }
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id 
                                                             ?? HttpContext.TraceIdentifier });
    }
}

Neste código injetamos o serviço IHttpContextAccessor no construtor , alteramos o método Action Privacy e criamos os métodos Action Segredo e AcessoUser definindo o atributo Authorize e definindo o perfil de acesso usando o atributo Roles.

Vamos agora ajustar o arquivo _Layout.cshml da pasta Views/Shared para exibir o link de login:

...
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<
ul class="navbar-nav flex-grow-1">
<
li class="nav-item">
   <
a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</
li>
<
li class="nav-item">
   <
a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@string.Format("{0}{1}",Context.Request.Path, Context.Request.QueryString)">Login</a>
</
li>
<
li class="nav-item">
 
<partial name="_LoginPartial" />
</
li>
</
ul>
</
div>
...

A partial view _LoginPartial possui o seguinte código :

@if(User.Identity.IsAuthenticated)
{
 
<form method="post" class="form-inline" asp-controller="Account" asp-action="Logout" asp-route-returnUrl="@string.Format("{0}{1}",Context.Request.Path, Context.Request.QueryString)">
<
button type="submit" class="btn btn-link">Logout</button> @User.Identity.Name
</form>
}

else

{
 
<a class="btn btn-link" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@string.Format("{0}{1}",Context.Request.Path, Context.Request.QueryString)">Login</a>
}

A seguir vamos criar na pasta Controllers um controlador chamado AccountController contendo o seguinte código:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using MvcRoleBased.Models;
namespace MvcRoleBased.Controllers;
public class AccountController : Controller
{
    [AllowAnonymous]
    public IActionResult Login(string returnUrl)
    {
        return View(new LoginViewModel()
        {
            ReturnUrl = returnUrl
        });
    }
    [AllowAnonymous]
    [HttpPost]
    public IActionResult Login(LoginViewModel loginVM)
    {
        if (!string.IsNullOrEmpty(loginVM.UserName) && 
             string.IsNullOrEmpty(loginVM.Password))
        {
            return RedirectToAction("Login");
        }

        ClaimsIdentity? identity = null;
        bool isAuthenticate = false;
        if (loginVM.UserName == "admin" && loginVM.Password == "numsey#2023")
        {
            identity = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name,loginVM.UserName),
                new Claim(ClaimTypes.Role,"Admin")
            }, CookieAuthenticationDefaults.AuthenticationScheme);
            isAuthenticate = true;
        }
        if (loginVM.UserName == "macoratti" && loginVM.Password == "numsey@2023")
        {
            identity = new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Name,loginVM.UserName),
                new Claim(ClaimTypes.Role,"User")
            }, CookieAuthenticationDefaults.AuthenticationScheme);
            isAuthenticate = true;
        }
        if (isAuthenticate)
        {
            var principal = new ClaimsPrincipal(identity);
            var perfil = principal.IsInRole("User");
            var login = HttpContext.SignInAsync(CookieAuthenticationDefaults
                                                        .AuthenticationScheme, principal);

            return RedirectToAction("AcessoUser", "Home");
        }
        ModelState.AddModelError("", "Falha ao realizar o login!!");
        return View();
    }
    public IActionResult AccessDenied(string returnUrl)
    {
        return View(new LoginViewModel()
        {
            ReturnUrl = returnUrl
        });
    }

    public IActionResult Logout()
    {
        HttpContext.SignOutAsync();
        return RedirectToAction("Index", "Home");
    }
}

No código acima, usamos a classe ClaimsIdentity, que contém as propriedades Claims como Name e Role. Aqui criamos duas contas, uma para administrador e outra para usuário. A classe ClaimsPrincipal tem a propriedade de identidades que retorna a coleção de ClaimsIdentity, isso significa que um usuário pode ter várias identidades.

Se a autenticação for bem-sucedida, criamos o objeto para ClaimsPrincipal e redirecionamos o usuário para a Action AcessoUser do controlador HomeController. (Esta Action usa o atributo Authorize permitindo o acesso ao perfil Admin e User)

Definimos os métodos Action Get para apresentar o formulário de Login e Post que vai postar as informações e onde iremos incluir o código para validar a autenticação do usuário.

Criamos também o método Action AccessDenied e Logout.

A seguir vamos criar as views usadas no projeto:

1- Login.cshtml

@model LoginViewModel
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <table class="table">
        <tr>
            <td><img src="~/images/login128.png" alt="Fazer o Login" class="center-block" /></td>
            <td>
                <div class="row">
                    <div class="col-12 col-md-12 col-lg-12">
                        <h3>Login</h3>
                        <br />
                        <form asp-controller="Account" asp-action="Login" method="post" 
                                                                   class="form-horizontal" role="form">
                            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
                            <input type="hidden" asp-for="ReturnUrl" />
                            <div class="form-group">
                                <label asp-for="UserName" class="col-md-2 control-label"></label>
                                <div class="col-md-6">
                                    <input asp-for="UserName" class="form-control" />
                                    <span asp-validation-for="UserName" class="text-danger"></span>
                                </div>
                            </div>
                            <div class="form-group" style="margin-top: 15px;">
                                <label asp-for="Password" class="col-md-2 control-label"></label>
                                <div class="col-md-6">
                                    <input asp-for="Password" class="form-control" id="Password" />
                                    <input type="checkbox" style="margin-top:10px" onclick="MostrarSenha()" />
                                                                                                                       Mostrar senha
                                    <span asp-validation-for="Password" class="text-danger"></span>
                                </div>
                            </div>
                            <br />
                            <div class="form-group">
                                <div class="col-md-8">
                                    <input type="submit" class="btn btn-primary" value="Fazer Log in" />
                                </div>
                            </div>
                        </form>
                    </div>
                </div>
            </td>
        </tr>
    </table>
</body>
</html>

Neste código temos uma view fortemente tipada usando a view model LoginViewModel onde temos a exibição de dois campos de entrada de dados: o nome do usuário e a senha.

Note que para a entrada da senha definimos um checkbox que pode ser marcado e desmarcardo e onde definimos o evento onclick que invoca a função javascript MostrarSenha(), e, que para o id na tag asp-for definimos o nome Password igual ao definido na View Model.

Precisamos criar esta função no arquivo site.js na pasta wwwroot/js usando o código a seguir:

...
function MostrarSenha() {
   var passwordInput = document.getElementById("Password");
   var currentType = passwordInput.getAttribute("type");
   if (currentType == "password") {
      passwordInput.setAttribute("type", "text");
   } else {password" }
}
 

No código verificamos o tipo atualmente definido para o input e alternamos entre os modos 'text' e 'password' para exibir/ocultar a senha.

Vamos criar na pasta wwwroot a pasta images e incluir nesta pasta vamos incluir as imagens usadas no projeto.

2- AccessDenied.cshtml

<div>
  <
img src="~/images/acessonegado1.jpg" alt="Fazer o Login"
                                
class="center-block" /></td>
</
div>
<
div class="text-center">
  <
h3>Você não tem autorização para acessar esta página</h3>
</
div>

3- AcessoUser.cshtml

<div>
  <
img src="~/images/secreto1.jpg" alt="Fazer o Login" class="center-block" />
</
div>
<
div class="text-center">
  @
if (ViewBag.UserName != null)
  {
    
<h4>(@ViewBag.UserName - @ViewBag.Role)</h4>
  }
<h3>Bem-Vindo a àrea do Usuário</h3>
</
div>

4- Segredo.cshtml

@{
    ViewData["Title"] = "Home Page";
}
<div>
   <img src="~/images/secreto1.jpg" alt="Fazer o Login" class="center-block" />
</div>
@if (ViewBag.UserName != null)
{
    <h4>(@ViewBag.UserName - @ViewBag.Role)</h4>
}
<div class="text-center">
    <h3>Bem-Vindo a àrea de Segurança</h3>
</div>

5- Index.cshtml

@{
  ViewData[
"Title"] = "Home Page";
}

<
div>
  <
img src="~/images/home.jpg" alt="Fazer o Login" class="center-block" />
</
div>
  @
if (ViewBag.UserName != null)
  {
   
<h4>(@ViewBag.UserName - @ViewBag.Role)</h4>
  }

  <
div class="text-center">
<
h3>Autorização baseada em Role</h3>
</
div>

6- Privacy.cshtml

@{
   ViewData[
"Title"] = "Privacy Policy";
}

<
div>
  <
img src="~/images/privacy1.jpg" alt="Fazer o Login" class="center-block" />
</
div>
@
if (ViewBag.UserName != null)
{
 
<h4>(@ViewBag.UserName - @ViewBag.Role)</h4>
}

<
div class="text-center">
<
h3>Página de Política de privacidade.</h3>
</
div>

Agora é só alegria...

Executando o projeto teremos o seguinte resultado:

E estamos conversados...

Pegue o projeto aqui:  MvcRoleBased.zip

"E do modo por que Moisés levantou a serpente no deserto, assim importa que o Filho do Homem seja levantado, para que todo o que nele crê tenha a vida eterna."
João 3:14-15

Referências:


José Carlos Macoratti