Autenticação e autorização no Blazor: um guia prático
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
HttpContextdo 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.