Adicionar autenticação ao seu aplicativo .NET Core (Blazor Server)
- A demonstração a seguir é construída no .NET Core 8.0. O SDK é compatível com .NET 6.0 ou superior.
- Os projetos de exemplo do .NET Core estão disponíveis no repositório GitHub.
Pré-requisitos
- Uma conta Logto Cloud ou um Logto auto-hospedado.
- Um aplicativo web tradicional Logto criado.
Instalação
Adicione o pacote NuGet ao seu projeto:
dotnet add package Logto.AspNetCore.Authentication
Integração
Adicionar autenticação Logto
Abra Startup.cs
(ou Program.cs
) e adicione o seguinte código para registrar os serviços de autenticação do Logto:
using Logto.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogtoAuthentication(options =>
{
options.Endpoint = builder.Configuration["Logto:Endpoint"]!;
options.AppId = builder.Configuration["Logto:AppId"]!;
options.AppSecret = builder.Configuration["Logto:AppSecret"];
});
O método AddLogtoAuthentication
fará as seguintes coisas:
- Definir o esquema de autenticação padrão para
LogtoDefaults.CookieScheme
. - Definir o esquema de desafio padrão para
LogtoDefaults.AuthenticationScheme
. - Definir o esquema de logout padrão para
LogtoDefaults.AuthenticationScheme
. - Adicionar manipuladores de autenticação de cookie e OpenID Connect ao esquema de autenticação.
Fluxos de login e logout
Antes de prosseguirmos, há dois termos confusos no middleware de autenticação do .NET Core que precisamos esclarecer:
- CallbackPath: O URI para o qual o Logto redirecionará o usuário após o usuário ter feito login (o "URI de redirecionamento" no Logto)
- RedirectUri: O URI para o qual será redirecionado após as ações necessárias terem sido realizadas no middleware de autenticação do Logto.
O processo de login pode ser ilustrado da seguinte forma:
Da mesma forma, o .NET Core também possui SignedOutCallbackPath e RedirectUri para o fluxo de logout.
Para maior clareza, nos referiremos a eles da seguinte forma:
Termo que usamos | Termo do .NET Core |
---|---|
URI de redirecionamento do Logto | CallbackPath |
URI de redirecionamento pós-logout do Logto | SignedOutCallbackPath |
URI de redirecionamento do aplicativo | RedirectUri |
Sobre o login baseado em redirecionamento
- Este processo de autenticação segue o protocolo OpenID Connect (OIDC), e o Logto aplica medidas de segurança rigorosas para proteger o login do usuário.
- Se você tiver vários aplicativos, pode usar o mesmo provedor de identidade (Logto). Uma vez que o usuário faz login em um aplicativo, o Logto completará automaticamente o processo de login quando o usuário acessar outro aplicativo.
Para saber mais sobre a lógica e os benefícios do login baseado em redirecionamento, veja Experiência de login do Logto explicada.
Configurar URIs de redirecionamento
Nos trechos de código a seguir, assumimos que seu aplicativo está sendo executado em http://localhost:3000/
.
Primeiro, vamos configurar o URI de redirecionamento do Logto. Adicione o seguinte URI à lista de "Redirect URIs" na página de detalhes do aplicativo Logto:
http://http://localhost:3000//Callback
Para configurar o URI de redirecionamento pós logout do Logto, adicione o seguinte URI à lista de "Post sign-out redirect URIs" na página de detalhes do aplicativo Logto:
http://http://localhost:3000//SignedOutCallback
Alterar os caminhos padrão
O URI de redirecionamento do Logto tem um caminho padrão de /Callback
, e o URI de redirecionamento pós logout do Logto tem um caminho padrão de /SignedOutCallback
.
Você pode deixá-los como estão se não houver nenhum requisito especial. Se você quiser alterá-los, pode definir a propriedade CallbackPath
e SignedOutCallbackPath
para LogtoOptions
:
builder.Services.AddLogtoAuthentication(options =>
{
// Outras configurações...
options.CallbackPath = "/Foo";
options.SignedOutCallbackPath = "/Bar";
});
Lembre-se de atualizar o valor na página de detalhes do aplicativo Logto de acordo.
Adicionar rotas
Como o Blazor Server usa SignalR para se comunicar entre o servidor e o cliente, isso significa que métodos que manipulam diretamente o contexto HTTP (como emitir desafios ou redirecionamentos) não funcionam como esperado quando chamados de um componente Blazor.
Para corrigir isso, precisamos adicionar explicitamente dois endpoints para redirecionamentos de login e logout:
app.MapGet("/SignIn", async context =>
{
if (!(context.User?.Identity?.IsAuthenticated ?? false))
{
await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});
app.MapGet("/SignOut", async context =>
{
if (context.User?.Identity?.IsAuthenticated ?? false)
{
await context.SignOutAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});
Agora podemos redirecionar para esses endpoints para acionar login e logout.
Implementar botões de login e logout
No componente Razor, adicione o seguinte código:
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
@* ... *@
<p>Está autenticado: @User.Identity?.IsAuthenticated</p>
@if (User.Identity?.IsAuthenticated == true)
{
<button @onclick="SignOut">Sair</button>
}
else
{
<button @onclick="SignIn">Entrar</button>
}
@* ... *@
@code {
private ClaimsPrincipal? User { get; set; }
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}
private void SignIn()
{
NavigationManager.NavigateTo("/SignIn", forceLoad: true);
}
private void SignOut()
{
NavigationManager.NavigateTo("/SignOut", forceLoad: true);
}
}
Explicação:
- O
AuthenticationStateProvider
injetado é usado para obter o estado de autenticação do usuário atual e preencher a propriedadeUser
. - Os métodos
SignIn
eSignOut
são usados para redirecionar o usuário para os endpoints de login e logout, respectivamente. Devido à natureza do Blazor Server, precisamos usar oNavigationManager
com carregamento forçado para acionar o redirecionamento.
A página mostrará o botão "Entrar" se o usuário não estiver autenticado e mostrará o botão "Sair" se o usuário estiver autenticado.
O componente <AuthorizeView />
Alternativamente, você pode usar o componente AuthorizeView
para renderizar condicionalmente o conteúdo com base no estado de autenticação do usuário. Este componente é útil quando você deseja mostrar conteúdo diferente para usuários autenticados e não autenticados.
No seu componente Razor, adicione o seguinte código:
@using Microsoft.AspNetCore.Components.Authorization
@* ... *@
<AuthorizeView>
<Authorized>
<p>Nome: @User?.Identity?.Name</p>
@* Conteúdo para usuários autenticados *@
</Authorized>
<NotAuthorized>
@* Conteúdo para usuários não autenticados *@
</NotAuthorized>
</AuthorizeView>
@* ... *@
O componente AuthorizeView
requer um parâmetro em cascata do tipo Task<AuthenticationState>
. Uma maneira direta de obter este parâmetro é adicionar o componente <CascadingAuthenticationState>
. No entanto, devido à natureza do Blazor Server, não podemos simplesmente adicionar o componente ao layout ou ao componente raiz (pode não funcionar como esperado). Em vez disso, podemos adicionar o seguinte código ao builder (Program.cs
ou Startup.cs
) para fornecer o parâmetro em cascata:
builder.Services.AddCascadingAuthenticationState();
Então, você pode usar o componente AuthorizeView
em todos os componentes que precisarem dele.
Ponto de verificação: Teste seu aplicativo
Agora, você pode testar seu aplicativo:
- Execute seu aplicativo, você verá o botão de login.
- Clique no botão de login, o SDK iniciará o processo de login e redirecionará você para a página de login do Logto.
- Após fazer login, você será redirecionado de volta para o seu aplicativo e verá o botão de logout.
- Clique no botão de logout para limpar o armazenamento local e sair.
Obter informações do usuário
Exibir informações do usuário
Para saber se o usuário está autenticado, você pode verificar a propriedade User.Identity?.IsAuthenticated
.
Para obter as reivindicações do perfil do usuário, você pode usar a propriedade User.Claims
:
var claims = User.Claims;
// Obter o ID do usuário
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;
Veja LogtoParameters.Claims
para a lista de nomes de reivindicações e seus significados.
Solicitar reivindicações adicionais
Você pode perceber que algumas informações do usuário estão faltando no objeto retornado de User.Claims
. Isso ocorre porque OAuth 2.0 e OpenID Connect (OIDC) são projetados para seguir o princípio do menor privilégio (PoLP), e o Logto é construído com base nesses padrões.
Por padrão, reivindicações limitadas são retornadas. Se você precisar de mais informações, pode solicitar escopos adicionais para acessar mais reivindicações.
Uma "reivindicação (Claim)" é uma afirmação feita sobre um sujeito; um "escopo (Scope)" é um grupo de reivindicações. No caso atual, uma reivindicação é uma informação sobre o usuário.
Aqui está um exemplo não normativo da relação escopo - reivindicação:
A reivindicação "sub" significa "sujeito (Subject)", que é o identificador único do usuário (ou seja, ID do usuário).
O Logto SDK sempre solicitará três escopos: openid
, profile
e offline_access
.
Para solicitar escopos adicionais, você pode configurar a propriedade Scopes
no objeto options
:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Scopes = new string[] {
LogtoParameters.Scopes.Email,
LogtoParameters.Scopes.Phone
}
});
Então você pode acessar as reivindicações adicionais via User.Claims
:
var claims = User.Claims;
// Obter o email do usuário
var email = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Email)?.Value;
Reivindicações que precisam de solicitação de rede
Para evitar sobrecarregar o objeto do usuário, algumas reivindicações requerem solicitações de rede para serem buscadas. Por exemplo, a reivindicação custom_data não está incluída no objeto do usuário, mesmo que seja solicitada nos escopos. Para buscar essas reivindicações, você pode definir GetClaimsFromUserInfoEndpoint
como true
no objeto options
:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.GetClaimsFromUserInfoEndpoint = true;
});
Escopos e reivindicações
Logto usa as convenções de escopos e reivindicações do OIDC para definir os escopos e reivindicações para recuperar informações do usuário do Token de ID e do endpoint userinfo do OIDC. Tanto "escopo" quanto "reivindicação" são termos das especificações OAuth 2.0 e OpenID Connect (OIDC).
Aqui está a lista de Escopos (Scopes) suportados e as Reivindicações (Claims) correspondentes:
openid
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
sub | string | O identificador único do usuário | Não |
profile
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
name | string | O nome completo do usuário | Não |
username | string | O nome de usuário do usuário | Não |
picture | string | URL da foto de perfil do Usuário Final. Este URL DEVE referir-se a um arquivo de imagem (por exemplo, um arquivo de imagem PNG, JPEG ou GIF), em vez de uma página da Web contendo uma imagem. Note que este URL DEVE referenciar especificamente uma foto de perfil do Usuário Final adequada para exibição ao descrever o Usuário Final, em vez de uma foto arbitrária tirada pelo Usuário Final. | Não |
created_at | number | Hora em que o Usuário Final foi criado. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z). | Não |
updated_at | number | Hora em que as informações do Usuário Final foram atualizadas pela última vez. O tempo é representado como o número de milissegundos desde a época Unix (1970-01-01T00:00:00Z). | Não |
Outras reivindicações padrão incluem family_name
, given_name
, middle_name
, nickname
, preferred_username
, profile
, website
, gender
, birthdate
, zoneinfo
e locale
também serão incluídas no escopo profile
sem a necessidade de solicitar o endpoint userinfo. Uma diferença em relação às reivindicações acima é que essas reivindicações só serão retornadas quando seus valores não estiverem vazios, enquanto as reivindicações acima retornarão null
se os valores estiverem vazios.
Ao contrário das reivindicações padrão, as reivindicações created_at
e updated_at
estão usando milissegundos em vez de segundos.
email
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
string | O endereço de email do usuário | Não | |
email_verified | boolean | Se o endereço de email foi verificado | Não |
phone
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
phone_number | string | O número de telefone do usuário | Não |
phone_number_verified | boolean | Se o número de telefone foi verificado | Não |
address
Por favor, consulte o OpenID Connect Core 1.0 para os detalhes da reivindicação de endereço.
custom_data
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
custom_data | object | Os dados personalizados do usuário | Sim |
identities
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
identities | object | As identidades vinculadas do usuário | Sim |
sso_identities | array | As identidades SSO vinculadas do usuário | Sim |
urn:logto:scope:organizations
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
organizations | string[] | Os IDs das organizações às quais o usuário pertence | Não |
organization_data | object[] | Os dados das organizações às quais o usuário pertence | Sim |
urn:logto:scope:organization_roles
Nome da Reivindicação | Tipo | Descrição | Precisa de userinfo? |
---|---|---|---|
organization_roles | string[] | Os papéis da organização aos quais o usuário pertence com o formato <organization_id>:<role_name> | Não |
Considerando o desempenho e o tamanho dos dados, se "Precisa de userinfo?" for "Sim", isso significa que a reivindicação não aparecerá no Token de ID, mas será retornada na resposta do endpoint userinfo.
Recursos de API
Recomendamos ler 🔐 Controle de Acesso Baseado em Papel (RBAC) primeiro para entender os conceitos básicos do RBAC do Logto e como configurar corretamente os recursos de API.
Configurar recurso de API no seu aplicativo
Depois de configurar os recursos de API, você pode adicioná-los ao configurar o Logto em seu aplicativo:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://<seu-indicador-de-recurso-de-api>";
});
Cada recurso de API tem suas próprias permissões (escopos).
Por exemplo, o recurso https://shopping.your-app.com/api
tem as permissões shopping:read
e shopping:write
, e o recurso https://store.your-app.com/api
tem as permissões store:read
e store:write
.
Para solicitar essas permissões, você pode adicioná-las ao configurar o Logto em seu aplicativo:
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://shopping.your-app.com/api";
options.Scopes = new string[] {
"openid",
"profile",
"offline_access",
"read",
"write"
};
});
Você pode notar que os escopos são definidos separadamente dos recursos de API. Isso ocorre porque Resource Indicators for OAuth 2.0 especifica que os escopos finais para a solicitação serão o produto cartesiano de todos os escopos em todos os serviços de destino.
Não há problema em solicitar escopos que não estão definidos nos recursos de API. Por exemplo, você pode solicitar o escopo email
mesmo que os recursos de API não tenham o escopo email
disponível. Escopos indisponíveis serão ignorados com segurança.
Após o login bem-sucedido, o Logto emitirá os escopos apropriados para os recursos de API de acordo com os papéis do usuário.
Buscar tokens
Buscar token via HttpContext
Às vezes, você pode precisar buscar o token de acesso ou o token de ID para chamadas de API. Você pode usar o método GetTokenAsync
para buscar os tokens:
var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessToken);
var idToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.IdToken);
Não há necessidade de se preocupar com a expiração do token, o middleware de autenticação irá automaticamente atualizar os tokens quando necessário.
Embora o middleware de autenticação atualize automaticamente os tokens, as reivindicações (Claims) no objeto do usuário não serão atualizadas devido à limitação do manipulador de autenticação OpenID Connect subjacente. Isso pode ser resolvido assim que escrevermos nosso próprio manipulador de autenticação.
Observe que o token de acesso acima é um token opaco (Opaque token) para o endpoint userinfo no OpenID Connect, que não é um token JWT. Se você especificou o recurso de API, precisa usar LogtoParameters.Tokens.AccessTokenForResource
para buscar o token de acesso para o recurso de API:
var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);
Este token será um token JWT com o recurso de API como o público (Audience).
Buscar token em componentes Razor
Como não podemos acessar diretamente o HttpContext
em componentes Razor, precisamos injetar o HttpContextAccessor
no componente e usá-lo para buscar os tokens. O código a seguir demonstra como buscar o token de acesso para o recurso de API em um componente Razor:
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@using Logto.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHttpContextAccessor HttpContextAccessor
@* ... *@
<p><b>Recurso:</b> @(Resource ?? "(null)")</p>
<p><b>Token de Acesso:</b> @(AccessToken ?? "(null)")</p>
@* ... *@
@code {
private ClaimsPrincipal? User { get; set; }
private string? AccessToken { get; set; }
private string? Resource { get; set; }
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
if (User?.Identity?.IsAuthenticated == true)
{
await FetchTokenAsync();
}
}
private async Task FetchTokenAsync()
{
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext == null)
{
return;
}
var logtoOptions = httpContext.GetLogtoOptions();
Resource = logtoOptions?.Resource;
// Substitua por outros tipos de token, se necessário
AccessToken = await httpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);
}
}