Autenticação e autorização no Blazor: um guia prático

· 13 min de leitura

Se você trabalhou com ASP.NET MVC ou Razor Pages, provavelmente tem um modelo mental de como a autenticação funciona: o middleware intercepta a solicitação, verifica um cookie ou token, preenche HttpContext.User e você está pronto para a corrida. O Blazor altera esse modelo mental de maneiras sutis, mas importantes — e se você não entender essas diferenças desde o início, acabará depurando problemas de autenticação que parecem impossíveis.

Nesta postagem, quero explicar como a autenticação e a autorização realmente funcionam no Blazor, abrangendo os modelos de hospedagem Server e WebAssembly. Iremos desde o básico até provedores personalizados, OAuth externo e as armadilhas que vi atrapalhar até mesmo desenvolvedores .NET experientes.

Por que o Auth no Blazor é diferente

No ASP.NET tradicional, cada interação do usuário é uma solicitação HTTP. O servidor valida as credenciais, define um cookie e cada solicitação subsequente carrega esse cookie. O pipeline de autenticação é linear e previsível.

Blazor Server opera em uma conexão SignalR persistente. Depois que a solicitação HTTP inicial carrega a página, todas as interações subsequentes acontecem por meio de WebSockets. Não há nenhuma nova solicitação HTTP para cada clique de botão, portanto o middleware não é executado novamente em cada interação. O HttpContext está disponível durante a conexão inicial, mas depender dele durante toda a vida útil de um circuito é uma receita para bugs.

Blazor WebAssembly é executado inteiramente no navegador. Não há nenhum HttpContext no lado do servidor. O estado de autenticação deve ser obtido de uma API, armazenado no lado do cliente e gerenciado por meio de tokens — normalmente JWTs. O servidor confia no cliente apenas no que diz respeito à validação do token.

Isso significa que o Blazor precisa de sua própria abstração para o estado de autenticação, que funcione independentemente do modelo de hospedagem. Essa abstração é o AuthenticationStateProvider.

Estado de autenticação: The Foundation

No coração do sistema de autenticação do Blazor está AuthenticationStateProvider. Esta é uma classe abstrata que expõe um único método crítico:

public abstract Task<AuthenticationState> GetAuthenticationStateAsync();

O objeto AuthenticationState envolve um ClaimsPrincipal — o mesmo modelo de identidade usado em todo o .NET. Os componentes não se comunicam diretamente com cookies ou tokens; eles perguntam ao AuthenticationStateProvider o estado atual.

Para disponibilizar esse estado para toda a sua árvore de componentes, o Blazor fornece CascadingAuthenticationState. Você normalmente envolve seu roteador com ele em App.razor ou em seu layout:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <p>You're not authorized to view this page.</p>
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Page not found.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

O AuthorizeRouteView está cumprindo uma função dupla aqui: verifica se o usuário está autenticado e autorizado antes de renderizar o componente da página correspondente e fornece uma UI substituta quando não está.

No .NET 8 e posterior com o modelo Blazor unificado, você configurará isso em seu App.razor e a estrutura manipulará o parâmetro em cascata automaticamente quando você usar AddCascadingAuthenticationState() em seu registro de serviço.

Integração de identidade ASP.NET

Para a maioria dos projetos, você não precisa criar a autenticação do zero. O ASP.NET Identity oferece gerenciamento de usuários, hash de senha, autenticação de dois fatores e confirmação de conta prontos para uso.A configuração com o Blazor começa em Program.cs:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    });

Com o modelo Blazor Web App no .NET 8+, a UI de identidade com scaffolding usa componentes Razor diretamente. Você obtém páginas de login, registro e gerenciamento de contas que se integram naturalmente ao restante do seu aplicativo Blazor - chega de mistura estranha de Razor Pages e componentes Blazor.

O ApplicationDbContext herda de IdentityDbContext e você precisará executar migrações para criar as tabelas de identidade:

public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}
dotnet ef migrations add InitialIdentity
dotnet ef database update

O componente AuthorizeView

Depois que o estado de autenticação estiver fluindo pela sua árvore de componentes, AuthorizeView permite renderizar a UI condicionalmente:

<AuthorizeView>
    <Authorized>
        <p>Welcome, @context.User.Identity?.Name!</p>
        <a href="/account/manage">Manage Account</a>
        <form method="post" action="/account/logout">
            <button type="submit">Log Out</button>
        </form>
    </Authorized>
    <NotAuthorized>
        <a href="/account/login">Log In</a>
        <a href="/account/register">Register</a>
    </NotAuthorized>
</AuthorizeView>

O parâmetro context dentro de <Authorized> dá acesso ao AuthenticationState, para que você possa inspecionar declarações, funções e a identidade do usuário diretamente em sua marcação.

Você também pode usar AuthorizeView com funções e políticas:

<AuthorizeView Roles="Admin,Moderator">
    <Authorized>
        <button @onclick="DeletePost">Delete Post</button>
    </Authorized>
</AuthorizeView>

<AuthorizeView Policy="CanEditArticles">
    <Authorized>
        <button @onclick="EditArticle">Edit</button>
    </Authorized>
</AuthorizeView>

Uma coisa a ter em mente: AuthorizeView é uma preocupação da interface do usuário. Oculta ou mostra elementos, mas não protege a lógica subjacente. Se alguém puder chamar seu endpoint de API ou invocar seu método diretamente, ele ignorará AuthorizeView completamente. Sempre imponha a autorização também no lado do servidor.

O atributo [Autorizar]

Para proteger uma página inteira, aplique o atributo [Authorize]:

@page "/admin/dashboard"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Dashboard</h1>
<p>Only administrators can see this page.</p>

Quando um usuário não autenticado navega para esta página, o AuthorizeRouteView entra em ação e renderiza o modelo <NotAuthorized> que você definiu anteriormente. Você pode redirecionar para uma página de login lidando com o caso NotAuthorized com navegação:

<NotAuthorized>
    @if (!context.User.Identity?.IsAuthenticated ?? true)
    {
        <RedirectToLogin />
    }
    else
    {
        <p>You don't have permission to access this page.</p>
    }
</NotAuthorized>

Um componente RedirectToLogin simples pode ser parecido com:

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var returnUrl = Uri.EscapeDataString(Navigation.Uri);
        Navigation.NavigateTo($"/account/login?returnUrl={returnUrl}", forceLoad: true);
    }
}

O forceLoad: true é importante aqui - você deseja uma navegação HTTP real para que o middleware de autenticação do lado do servidor possa lidar com o fluxo de login corretamente.

Autorização baseada em funções e políticas

As funções são o modelo mais simples: atribua usuários a grupos como “Administrador” ou “Editor” e verifique a associação. Mas as políticas oferecem muito mais flexibilidade.

Registre políticas em Program.cs:

builder.Services.AddAuthorizationCore(options =>
{
    options.AddPolicy("CanPublish", policy =>
        policy.RequireClaim("Permission", "Publish"));

    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    options.AddPolicy("PremiumUser", policy =>
        policy.RequireRole("Premium")
              .RequireClaim("Subscription", "Active"));
});

Os requisitos personalizados precisam de um manipulador:

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("DateOfBirth");

        if (dateOfBirthClaim is null)
            return Task.CompletedTask;

        var dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
        var age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;

        if (age >= requirement.MinimumAge)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

Registre o manipulador:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

Nos componentes, você também pode verificar a autorização programaticamente quando precisar de lógica dinâmica:

@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider

@code {
    private bool canPublish;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var result = await AuthorizationService.AuthorizeAsync(
            authState.User, "CanPublish");
        canPublish = result.Succeeded;
    }
}

Provedores OAuth externos

O suporte para “Fazer login com o Google” ou “Fazer login com GitHub” é simples com o middleware de autenticação ASP.NET. Eles são configurados no lado do servidor, pois o fluxo OAuth requer redirecionamentos HTTP.

builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
        options.Scope.Add("profile");
    })
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
    })
    .AddGitHub(options =>
    {
        options.ClientId = builder.Configuration["Auth:GitHub:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:GitHub:ClientSecret"]!;
    });

Para GitHub, você precisará do pacote AspNet.Security.OAuth.GitHub NuGet, pois ele não está incluído nas bibliotecas ASP.NET padrão.

A IU de login fornece links que acionam o desafio externo:

@page "/account/external-login"

<h2>Sign in with an external provider</h2>

<form method="post" action="/api/auth/external-login">
    <button type="submit" name="provider" value="Google">
        Sign in with Google
    </button>
    <button type="submit" name="provider" value="Microsoft">
        Sign in with Microsoft
    </button>
    <button type="submit" name="provider" value="GitHub">
        Sign in with GitHub
    </button>
</form>

O endpoint da API aciona o desafio e lida com o retorno de chamada:

app.MapPost("/api/auth/external-login", (string provider, HttpContext context) =>
{
    var properties = new AuthenticationProperties
    {
        RedirectUri = "/api/auth/external-callback"
    };
    return Results.Challenge(properties, [provider]);
});

A autenticação externa no Blazor sempre requer uma navegação de página inteira – você não pode concluir um redirecionamento OAuth dentro de um circuito SignalR ou de um aplicativo WebAssembly sem passar pelo servidor.

Autenticação baseada em token no Blazor WebAssemblyO Blazor WebAssembly é executado no cliente, portanto, a autenticação baseada em cookies não se aplica da mesma maneira. Em vez disso, normalmente você usa JWTs armazenados na memória e anexados a solicitações HTTP de saída.

A estrutura fornece AuthorizationMessageHandler para anexar tokens automaticamente:

builder.Services.AddHttpClient("API",
    client => client.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler<AuthorizationMessageHandler>();

builder.Services.AddScoped(sp =>
    sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));

Para aplicativos Blazor WASM autônomos que são autenticados em sua própria API, você implementará um AuthenticationStateProvider personalizado que analisa o JWT:

public class JwtAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly ILocalStorageService _localStorage;
    private readonly HttpClient _httpClient;

    public JwtAuthenticationStateProvider(
        ILocalStorageService localStorage,
        HttpClient httpClient)
    {
        _localStorage = localStorage;
        _httpClient = httpClient;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _localStorage.GetItemAsync<string>("authToken");

        if (string.IsNullOrWhiteSpace(token))
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));

        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var claims = ParseClaimsFromJwt(token);
        var identity = new ClaimsIdentity(claims, "jwt");

        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void NotifyAuthStateChanged()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        var payload = jwt.Split('.')[1];

        var padded = payload.Length % 4 switch
        {
            2 => payload + "==",
            3 => payload + "=",
            _ => payload
        };

        var bytes = Convert.FromBase64String(padded);
        var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(bytes);

        return json?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())) 
               ?? Enumerable.Empty<Claim>();
    }
}

Após um login bem-sucedido, você armazena o token e notifica o estado de autenticação:

public class AuthService
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorageService _localStorage;
    private readonly JwtAuthenticationStateProvider _authStateProvider;

    public AuthService(
        HttpClient httpClient,
        ILocalStorageService localStorage,
        AuthenticationStateProvider authStateProvider)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
        _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
    }

    public async Task<bool> LoginAsync(string email, string password)
    {
        var response = await _httpClient.PostAsJsonAsync("/api/auth/login",
            new { Email = email, Password = password });

        if (!response.IsSuccessStatusCode)
            return false;

        var result = await response.Content
            .ReadFromJsonAsync<LoginResponse>();

        await _localStorage.SetItemAsync("authToken", result!.Token);
        _authStateProvider.NotifyAuthStateChanged();

        return true;
    }

    public async Task LogoutAsync()
    {
        await _localStorage.RemoveItemAsync("authToken");
        _authStateProvider.NotifyAuthStateChanged();
    }
}

Uma palavra de cautela: armazenar JWTs em localStorage os expõe a ataques XSS. Para aplicativos de maior segurança, considere manter os tokens apenas na memória e usar tokens de atualização ou adotar o padrão Backend-for-Frontend (BFF), onde o servidor gerencia tokens e o cliente usa cookies somente HTTP.

Blazor Server vs. WebAssembly: considerações de segurança

O modelo de hospedagem muda fundamentalmente sua postura de segurança.

Blazor Server mantém toda a lógica do seu componente no servidor. O cliente só vê diferenças de HTML renderizadas no SignalR. Isso significa:

  • A lógica sensível nunca sai do servidor
  • Você pode acessar bancos de dados e serviços internos diretamente dos componentes
  • O estado de autenticação vem do HttpContext do servidor na conexão inicial
  • O circuito pode sobreviver ao cookie de autenticação — se o cookie de um usuário expirar, o circuito permanecerá ativo até ser desconectado
  • Você deve lidar com a desconexão do circuito normalmente e revalidar o estado de autenticação na reconexão

Blazor WebAssembly é executado inteiramente no navegador. Isso significa:

  • Todo o código do seu componente pode ser baixado e inspecionado
  • Nunca coloque segredos, cadeias de conexão ou lógica de negócios confidenciais em componentes WASM
  • A autenticação só é aplicada no cliente para UX; a aplicação real deve acontecer na sua camada API
  • O gerenciamento de tokens é de sua responsabilidade
  • Considere usar o modelo hospedado onde um projeto de servidor lida com autenticação e atende o aplicativo WASM

Um padrão que recomendo para aplicativos WebAssembly é tratar cada componente como se fosse uma “UI não confiável” e cada endpoint de API como se estivesse sendo chamado por um cliente desconhecido. Valide tudo do lado do servidor, independentemente do que o cliente verifique.

Construindo um AuthenticationStateProvider personalizado

Às vezes, os provedores integrados não se adaptam à sua arquitetura. Talvez você esteja integrando um sistema de autenticação legado ou precise pesquisar alterações no estado de autenticação. Aqui está um provedor personalizado mais completo para Blazor Server:

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IUserService _userService;

    public CustomAuthStateProvider(
        IHttpContextAccessor httpContextAccessor,
        IUserService userService)
    {
        _httpContextAccessor = httpContextAccessor;
        _userService = userService;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var httpContext = _httpContextAccessor.HttpContext;

        if (httpContext?.User?.Identity?.IsAuthenticated != true)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var user = await _userService.GetUserWithClaimsAsync(userId);

        if (user is null)
            return new AuthenticationState(
                new ClaimsPrincipal(new ClaimsIdentity()));

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id),
            new(ClaimTypes.Name, user.DisplayName),
            new(ClaimTypes.Email, user.Email)
        };

        claims.AddRange(user.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
        claims.AddRange(user.Permissions.Select(p => new Claim("Permission", p)));

        var identity = new ClaimsIdentity(claims, "Custom");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public void MarkUserAsAuthenticated(string userId)
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void MarkUserAsLoggedOut()
    {
        var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymous));
        NotifyAuthenticationStateChanged(authState);
    }
}

Registre-o em Program.cs:

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<CustomAuthStateProvider>();

O principal insight aqui é NotifyAuthenticationStateChanged - chamá-lo aciona o parâmetro em cascata para atualização, que reavalia cada AuthorizeView e AuthorizeRouteView em sua árvore de componentes. É assim que você faz a UI reagir aos eventos de login/logout sem uma atualização completa da página.

Armadilhas e soluções comuns

Depois de trabalhar com autenticação do Blazor em muitos projetos, aqui estão os problemas que vejo com mais frequência:

1. Usando HttpContext nos componentes do servidor BlazorHttpContext está disponível durante a solicitação HTTP inicial, mas é null ou obsoleto durante as interações do SignalR. Não injete IHttpContextAccessor em componentes executados após a renderização inicial.

Solução: capture o que você precisa de HttpContext durante a inicialização e armazene-o em um serviço com escopo definido:

public class UserContext
{
    public string? UserId { get; set; }
    public string? AccessToken { get; set; }
}

// In a component that renders during the initial HTTP request:
@inject IHttpContextAccessor HttpContextAccessor
@inject UserContext UserContext

@code {
    protected override void OnInitialized()
    {
        var context = HttpContextAccessor.HttpContext;
        UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
        UserContext.AccessToken = context?.Request.Headers.Authorization
            .ToString().Replace("Bearer ", "");
    }
}

2. Estado de autenticação não atualizado após login

Você chama sua API de login, ela é bem-sucedida, mas a IU ainda mostra “Log In”.

Solução: Você deve chamar NotifyAuthenticationStateChanged em seu AuthenticationStateProvider depois que o estado de autenticação for alterado. A estrutura não detecta magicamente que um token foi armazenado ou um cookie foi definido.

3. Atributo de autorização que não funciona em componentes

Você adiciona [Authorize] a um componente, mas ele não bloqueia usuários não autenticados.

Solução: Certifique-se de usar AuthorizeRouteView em vez de RouteView simples em seu App.razor. O padrão RouteView ignora totalmente os atributos de autorização.

4. A pré-renderização quebra o estado de autenticação

Durante a pré-renderização do lado do servidor no Blazor WebAssembly, não há token de autenticação disponível. Os componentes são renderizados como não autenticados e, em seguida, passam para o estado autenticado após o carregamento do WASM.

Solução: desative a pré-renderização para páginas sensíveis à autenticação com @rendermode InteractiveWebAssembly (sem pré-renderização) ou administre o estado de carregamento normalmente:

<AuthorizeView>
    <Authorized>
        <p>Welcome back, @context.User.Identity?.Name</p>
    </Authorized>
    <Authorizing>
        <p>Loading...</p>
    </Authorizing>
    <NotAuthorized>
        <a href="/login">Sign in</a>
    </NotAuthorized>
</AuthorizeView>

5. Expiração de token em circuitos de longa duração

Os circuitos do Blazor Server podem permanecer ativos por horas. Se o seu token ou sessão expirar, o usuário permanecerá “autenticado” na IU, mas as chamadas de API começarão a falhar.

Solução: implemente uma verificação periódica ou use um RevalidatingServerAuthenticationStateProvider:

public class RevalidatingAuthStateProvider
    : RevalidatingServerAuthenticationStateProvider
{
    private readonly IServiceScopeFactory _scopeFactory;

    public RevalidatingAuthStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory)
        : base(loggerFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var userManager = scope.ServiceProvider
            .GetRequiredService<UserManager<IdentityUser>>();

        var user = await userManager.GetUserAsync(authenticationState.User);
        return user is not null;
    }
}

Isso valida que o usuário ainda existe (e seu carimbo de segurança não mudou) a cada 30 minutos.

Conclusão

A autenticação e a autorização no Blazor exigem uma mudança de pensamento em relação ao ASP.NET tradicional de solicitação e resposta. A abstração AuthenticationStateProvider é a chave para entender como tudo se encaixa - uma vez que você internaliza isso, o resto segue naturalmente.

Para a maioria dos aplicativos, comece com o ASP.NET Identity e os modelos integrados. Eles lidam com o trabalho pesado de gerenciamento de usuários, hash de senha e geração de token. Adicione políticas e autorização baseada em declarações à medida que seus requisitos aumentam. Adicione provedores OAuth externos quando seus usuários esperarem.

O modelo de hospedagem é importante: o Blazor Server oferece uma postura de segurança mais tradicional, onde o código permanece no servidor, enquanto o WebAssembly leva você a pensar primeiro na API, onde o cliente não é confiável por design. Nenhum deles é inerentemente mais seguro – eles apenas têm modelos de ameaças diferentes.

Qualquer que seja a abordagem escolhida, lembre-se da regra de ouro: a autorização na UI é para a experiência do usuário, a autorização no servidor é para segurança. Sempre aplique ambas.