Usando o Identity Server 4 no Net Core 3.0

1. Introdução


Um dos meus projetos suportados recentemente enfrentou a tarefa de analisar a possibilidade de migrar do .NET framework 4.5 para o .Net Core no caso de necessidade de refatoração e cobrança de uma grande quantidade de dívida técnica acumulada. A escolha recaiu sobre a plataforma de destino .NET Core 3.0, pois, de acordo com os desenvolvedores da Microsoft, com o lançamento da versão 3.0, as etapas necessárias para a migração do código legado diminuirão várias vezes. Especialmente, fomos atraídos pelos planos de saída do EntityFramework 6.3 para .Net Core, ou seja, a maior parte do código baseado no EF 6.2 pode ser deixada "como está" em um projeto migrado no net core.


Com o nível de dados, ao que parece, ficou claro, no entanto, outra grande parte da portabilidade de código foi o nível de segurança, que, infelizmente, após uma rápida auditoria, você terá que jogá-lo quase completamente fora e reescrevê-lo do zero. Felizmente, o projeto já utilizava parte do ASP NET Identity, na forma de armazenamento de usuários e outras “bicicletas” presas ao lado.


Isso levanta a questão lógica: se a parte de segurança precisa fazer muitas alterações, por que não implementar imediatamente as abordagens recomendadas na forma de padrões do setor, a saber: levar o aplicativo a usar Open Id connect e OAuth usando a estrutura IdentityServer4 .


Problemas e soluções


Portanto, nos foi dado: existe um aplicativo JavaScript em Angular (cliente em termos IS4), ele usa um determinado subconjunto de WebAPI (Recursos), há também um banco de dados de identidade ASP NET desatualizado com logins de usuário que devem ser reutilizados após a atualização (para não iniciar todos os outros vezes) e, em alguns casos, é necessário dar a oportunidade de fazer login no sistema através da autenticação do Windows no lado do IdentityServer4. I.e. Há momentos em que os usuários trabalham através de uma rede local em um domínio do ActiveDirectory.


A principal solução para a migração de dados do usuário é escrever manualmente (ou usar ferramentas automatizadas) um script de migração entre o antigo e o novo esquema de dados de identidade. Por sua vez, usamos o aplicativo de comparação de esquema de dados automatizado e geramos um script SQL, dependendo da versão do Identity, o script de migração de destino conterá instruções de atualização diferentes. O principal aqui é não se esqueça de coordenar a tabela EFMigrationsHistory se o EF tiver sido usado antes e estiver planejado no futuro, por exemplo, expandir a entidade IdentityUser para campos adicionais.


Mas como configurar corretamente o IdentityServer4 agora e configurá-lo junto com as contas do Windows será descrito abaixo.


Plano de implementação


Por motivos de NDA, não descreverei como conseguimos implementar o IS4 em nosso projeto. No entanto, neste artigo, mostrarei em um site simples do ASP.NET Core criado do zero as etapas necessárias para obter um aplicativo totalmente configurado e funcional que usa o IdentityServer4 para fins de autorização e autenticação.
Para realizar o comportamento desejado, precisamos executar as seguintes etapas:


  • Crie um projeto vazio do ASP.Net Core e configure para usar o IdentityServer4.
  • Adicione um cliente como um aplicativo Angular.
  • Faça login através do open-id-connect google
  • Adicionar opção de autenticação do Windows

Por motivos de brevidade, todos os três componentes (IdentityServer, WebAPI, cliente Angular) estarão no mesmo projeto. O tipo selecionado de interação entre o cliente e o IdentityServer (GrantType) é um fluxo implícito, quando o access_token é passado para o lado do aplicativo no navegador e, em seguida, usado ao interagir com a WebAPI. Mais perto do lançamento, a julgar pelas alterações no repositório do ASP.NET Core, o fluxo implícito será substituído pelo Código de autorização + PKCE.)


No processo de criação e modificação do aplicativo, a interface de linha de comando do .NET Core será amplamente usada. Ela deve ser instalada no sistema no local com a versão mais recente da visualização do Core 3.0 (no momento da redação do artigo 3.0.100-preview7-012821).


Criação e configuração de um projeto web


O lançamento do IdentityServer versão 4 foi marcado pelo corte completo da interface do usuário a partir dessa estrutura. Agora, os desenvolvedores têm todo o direito de determinar a interface principal do servidor de autorização. Existem várias maneiras. Um dos mais populares é usar a interface do usuário no pacote QuickStart UI, que pode ser encontrado no repositório oficial no github .


Outra maneira, não menos conveniente, é a integração com a UI de identidade principal do ASP NET. Nesse caso, o desenvolvedor precisa configurar corretamente o middleware correspondente no projeto. Este método será descrito mais adiante.


Vamos começar criando um projeto simples da Web. Para fazer isso, execute a seguinte instrução na linha de comando:


dotnet new webapp -n IdentityServer4WebApp 

Após a execução, a saída será uma estrutura de aplicativo da web, que será gradualmente levada ao estado que precisamos. Aqui você precisa fazer uma reserva de que o .Net Core 3.0 for Identity usa RazorPages mais leves, ao contrário do MVC pesado.
Agora você precisa adicionar o suporte ao IdentityServer ao nosso projeto. Para fazer isso, instale os pacotes necessários:


 dotnet add package Microsoft.AspNetCore.ApiAuthorization.IdentityServer -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore -v 3.0.0-preview7.19365.7 dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.0.0-preview7.19362.6 dotnet add package Microsoft.EntityFrameworkCore.Sqlite -v 3.0.0-preview7.19362.6 

Além de links para pacotes de servidores de autorização, adicionamos aqui o suporte ao Entity Framework para armazenar informações do usuário no ecossistema Identity. Para simplificar, usaremos o banco de dados SQLite.


Para inicializar o banco de dados, crie nosso modelo de usuário e contexto de banco de dados. Para isso, declaramos duas classes ApplicationUser, herdadas de IdentityUser na pasta Models e ApplicationDbContext , herdadas de: ApiAuthorizationDbContext na pasta Data.

Em seguida, você precisa configurar o uso do contexto EntityFramework e criar o banco de dados. Para fazer isso, escrevemos o contexto no método ConfigureServices da classe Startup:


 public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddRazorPages(); } 

E adicione a cadeia de conexão a appsettings.json


 "ConnectionStrings": { "DefaultConnection": "Data Source=data.db" }, 

Agora você pode criar a migração inicial e inicializar o esquema do banco de dados. É importante notar que a ferramenta instalada para o ef core é necessária (para a visualização em questão, é necessária a versão 3.0.0-preview7.19362.6).


 dotnet ef migrations add Init dotnet ef database update 

Se todas as etapas anteriores foram concluídas sem erros, o arquivo de dados SQLite data.db deve aparecer no seu projeto.


Nesse estágio, podemos configurar e testar totalmente a capacidade de usar o Asp.Net Core Identity. Para fazer isso, faça alterações nos métodos de inicialização. Configure e Startup.ConfigureServices .


 //Startup.ConfigureServices: services.AddDefaultIdentity<ApplicationUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); //Startup. Configure: app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); 

Com essas linhas, incorporamos a possibilidade de autenticação e autorização no pipeline de processamento de solicitações. E também adicione a interface do usuário padrão para Identity.
Resta apenas corrigir a interface do usuário, adicionar à Pages \ Shared uma nova visualização do Razor com o nome _LoginPartial.cshtml e o seguinte conteúdo:


 @using IdentityServer4WebApp.Models @using Microsoft.AspNetCore.Identity @inject SignInManager<ApplicationUser> SignInManager @inject UserManager<ApplicationUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul> 

O código de apresentação acima deve adicionar links à área da interface Identity com controles de usuário integrados no painel de navegação (login e senha, registro, etc.)


Para obter a renderização de novos itens de menu, simplesmente modificamos o arquivo _Layout.cshtml adicionando a renderização dessa exibição parcial.


  <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> </ul> </div> <partial name="_LoginPartial" /> <!––––> </div> 

E agora vamos tentar executar nosso aplicativo e clicar nos links que aparecem na cabeça
No menu, o usuário deve ver uma página com boas-vindas e solicitação
digite login e senha. Nesse caso, você pode se registrar e fazer login - todos
deve funcionar.



Os desenvolvedores do IdentityServer4 fizeram um excelente trabalho para melhorar a integração do ASP.NET Identity e a própria estrutura do servidor. Para adicionar a capacidade de usar tokens OAuth2, você precisa complementar nosso projeto com algumas novas instruções no código.


Na penúltima linha do método Startup.ConfigureServices, adicione a configuração das convenções IS4 sobre a identidade principal do ASP.NET:


 services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); 

O método AddApiAuthorization instrui a estrutura a usar uma configuração específica suportada, principalmente por meio do arquivo appsettings.json. No momento, os recursos internos de gerenciamento do IS4 não são tão flexíveis e devem ser considerados como um ponto de partida para a criação de seus aplicativos. De qualquer forma, você pode usar a versão sobrecarregada desse método e configurar os parâmetros com mais detalhes por meio de retorno de chamada.


Em seguida, chamamos o método helper, que configura o aplicativo para verificar os tokens JWT emitidos pela estrutura.


 services.AddAuthentication() .AddIdentityServerJwt(); 

Por fim, no método Startup.Configure, adicione middleware para
Fornecendo pontos de extremidade Open ID Connect


 app.UseAuthentication(); app.UseAuthorization(); app.UseIdentityServer();//<- 

Como mencionado acima, os métodos auxiliares usados ​​leem a configuração em
arquivo de configurações do aplicativo appsettings.json , no qual devemos adicionar um novo
Seção IdentityServer .


 "IdentityServer": { "Clients": { "TestIdentityAngular": { "Profile": "IdentityServerSPA" } } } 

Nesta seção, um cliente é definido com o nome TestIdentityAngular, que atribuiremos ao futuro cliente do navegador e a um perfil de configuração específico.


O Application Profiles é uma nova ferramenta de configuração do IdentityServer que fornece várias configurações predefinidas com a capacidade de refinar determinados parâmetros. Usaremos o perfil IdentityServerSPA , projetado para casos em que o cliente do navegador e a estrutura estão localizados no mesmo projeto e têm os seguintes parâmetros:


  • O recurso redirect_uri definido como / authentication / login-callback .
  • Recurso post_logout_redirect_uri, definido como / authentication / logout-callback.
  • O conjunto de áreas inclui openid, profile, para cada recurso da API do aplicativo.
  • Conjunto de tipos de resposta OIDC permitidos - token id_token
  • GrantType para o cliente - implícito

Outros perfis possíveis são SPA (aplicativo sem IS4), IdentityServerJwt (API compartilhada com IS4), API (API separada).


Além disso, a configuração registra recursos:


  • ApiResources: um recurso da API denominado << appname >> API com propriedades para todos os clientes (*).
  • IdentityServerResources: IdentityResources.OpenId () e IdentityResources.Profile ()

Como você sabe, o IdentityServer usa certificados para assinar tokens, seus parâmetros também podem ser definidos no arquivo de configuração, portanto, no momento do teste, podemos usar
certificado de teste x509, para isso, é necessário especificá-lo na seção "Chave" do arquivo appsettings.Development.json .


 "IdentityServer": { "Key": { "Type": "Development" } } 

Agora, podemos dizer que o back-end que permite usar o IdentityServer está pronto e você pode começar a implementar o aplicativo do navegador.


Implementação de cliente angular


Nosso SPA baseado em navegador será escrito na plataforma Angular. O aplicativo conterá duas páginas, uma para usuários não autorizados e outra para usuários autenticados. Os exemplos usam a versão 8.1.2


Primeiro, crie a estrutura futura:


 ng new ClientApp 

No processo de criação, você precisa responder "sim" à proposta para usar o roteamento. E estilize um pouco a página na biblioteca de inicialização:


 cd ClientApp ng add bootstrap 

Em seguida, você precisa adicionar o suporte de hospedagem SPA ao nosso aplicativo principal. Primeiro, você precisa corrigir o projeto csproj - adicione informações sobre nosso aplicativo de navegador.


 <PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <IsPackable>false</IsPackable> <SpaRoot>ClientApp\</SpaRoot> <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes> <BuildServerSideRenderer>false</BuildServerSideRenderer> </PropertyGroup><ItemGroup> <Content Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> </Target> 

Depois disso, instale o pacote de nuget especial para dar suporte aos aplicativos do navegador.


 dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7 

E nós usamos seus métodos auxiliares:


 //Startup. ConfigureServices: services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; }); //Startup. Configure: app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); 

Além de chamar novos métodos, você deve excluir as páginas Razor Index.chtml e _ViewStart.chtml para que os serviços do SPA agora forneçam o conteúdo.


Se tudo tiver sido feito de acordo com as instruções, quando o aplicativo iniciar, a página padrão será exibida na tela.



Agora você precisa configurar o roteamento, para isso adicionamos ao projeto 2
componente:


 ng generate component Home -t=true -s=true --skipTests=true ng generate component Data -t=true -s=true --skipTests=true 

Nós os escrevemos na tabela de roteamento:


 const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, { path: 'data', component: DataComponent } ]; 

E modificamos o arquivo app.component.html para exibir corretamente os itens de menu.


 <header> <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'> <div class="container"> <a class="navbar-brand" [routerLink]='["/"]'>Client App</a> <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'> <ul class="navbar-nav flex-grow"> <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'> <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a> </li> <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/data"]'>Web api data</a> </li> </ul> </div> </div> </nav> </header> <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> </div> <div class="router-outlet"> <router-outlet></router-outlet> </div> 

Nesta etapa, você pode concluir a preparação básica da estrutura do aplicativo para implementar a interação por meio de tokens emitidos pelo IdentityServer.


O estágio atual de preparação da estrutura do nosso SPA pode ser chamado de concluído e agora devemos começar a implementar o módulo responsável por interagir com a parte do servidor usando os protocolos OpenID Connect e OAuth. Felizmente, os desenvolvedores da Microsoft já implementaram esse código e agora você pode simplesmente emprestar esse módulo deles. Como meu artigo foi escrito com base no pré-lançamento 7 do ASP.NET Core 3.0, pegaremos todo o código usando a tag de lançamento “v3.0.0-preview7.19365.7” no github .


Antes de importar o código, você deve instalar a biblioteca oidc-client , que
fornece muitas interfaces para aplicativos de navegador, bem como
suporta o gerenciamento de sessões do usuário e tokens de acesso. Para
Para começar a trabalhar com ele, você precisa instalar o pacote apropriado.


 npm install oidc-client@1.8.0 

Agora, em nosso SPA, é necessário implementar um módulo que encapsule a interação completa de acordo com os protocolos necessários. Para fazer isso, você precisa pegar o módulo ApiAuthorizationModule inteiro do rótulo do repositório ASP.NET Core acima e adicionar todos os seus arquivos ao aplicativo.


Além disso, você deve importá-lo para o módulo principal do aplicativo AppModule:


 @NgModule({ declarations: [ AppComponent, HomeComponent, DataComponent ], imports: [ BrowserModule, HttpClientModule, ApiAuthorizationModule,//<- AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

Para exibir novos itens de menu no módulo importado, existe um componente de menu de login do aplicativo ,
pode ser completamente alterado para atender às suas necessidades e adicionar
um link para ele na seção de navegação da visualização app.component.html .


O módulo de autorização da API para configurar a conexão OpenID do cliente SPA deve usar um terminal especial no back-end do aplicativo, para sua implementação
deve seguir estas etapas:


  1. Corrija o ID do cliente de acordo com o que definimos no arquivo de configuração appsettings.json na seção IdentityServer: Clients, no nosso caso, é TestIdentityAngular, está escrito na primeira linha do conjunto constante api-authorization.constants.ts.
  2. Adicione um controlador OidcConfigurationController que retornará diretamente a configuração ao aplicativo do navegador

O código do controlador criado é apresentado abaixo:


  [ApiController] public class OidcConfigurationController: ControllerBase { private readonly IClientRequestParametersProvider _clientRequestParametersProvider; public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider) { _clientRequestParametersProvider = clientRequestParametersProvider; } [HttpGet("_configuration/{clientId}")] public IActionResult GetClientRequestParameters([FromRoute]string clientId) { var parameters = _clientRequestParametersProvider.GetClientParameters(HttpContext, clientId); return Ok(parameters); } } 

Você também precisa configurar o suporte da API de ponto para o aplicativo de back-end.


 //Startup.ConfigureServices: services.AddControllers();//<-  services.AddRazorPages(); //Startup. Configure: app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllers(); }); 

Agora é hora de iniciar o aplicativo. Dois itens devem aparecer na página principal no menu superior - Login e registro . Além disso, na inicialização, o módulo de autorização importado solicitará do lado do servidor a configuração do cliente, que será levada em consideração no protocolo. Um exemplo de saída de configuração é mostrado abaixo:


 { "authority": "https://localhost:44367", "client_id": "TestIdentityAngular", "redirect_uri": "https://localhost:44367/authentication/login-callback", "post_logout_redirect_uri": "https://localhost:44367/authentication/logout-callback", "response_type": "id_token token", "scope": "IdentityServer4WebAppAPI openid profile" } 

Como você pode ver, o cliente durante a interação espera receber o token de identificação e o token de acesso, além de estar configurado para a área de acesso à nossa API.


Agora, se selecionarmos o item de menu Login , devemos ser redirecionados para a página do nosso IdentityServer4 e aqui podemos inserir o login e a senha e, se estiverem corretos, seremos imediatamente transferidos de volta para o aplicativo do navegador, que por sua vez receberá id_token e access_token . Como você pode ver abaixo, o próprio componente app-login-menu determinou que a autorização foi concluída com êxito e exibiu uma "saudação", além de um botão para Logout .



Ao abrir as "ferramentas de desenvolvedor" no navegador, é possível ver nos bastidores toda a interação usando o protocolo OIDC / OAuth. Isso está obtendo informações do servidor de autorização
via atividade de sessão de ponto final .bem conhecido / configuração aberta e pooling por meio do ponto de acesso de conexão / sessão de verificação. Além disso, o módulo de autorização é configurado para o mecanismo de “atualização silenciosa de tokens”, quando quando o token de acesso expira, o sistema passa independentemente as etapas de autorização em um iframe oculto. Você pode desativar as atualizações automáticas de tokens configurando o valor do parâmetro includeIdTokenInSilentRenew como "false" no arquivo authorize.service.ts .


Agora você pode lidar com a restrição de acesso a usuários não autorizados a partir dos componentes do aplicativo SPA, além de alguns controladores de API na parte traseira. Para demonstrar alguma API, criaremos uma classe ExchangeRateItem na pasta Models , bem como um controlador na pasta Controller que retornará alguns dados aleatórios.


 //Controller: [ApiController] public class ExchangeRateController { private static readonly string[] Currencies = new[] { "EUR", "USD", "BGN", "AUD", "CNY", "TWD", "NZD", "TND", "UAH", "UYU", "MAD" }; [HttpGet("api/rates")] public IEnumerable<ExchangeRateItem> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new ExchangeRateItem { FromCurrency = "RUR", ToCurrency = Currencies[rng.Next(Currencies.Length)], Value = Math.Round(1.0+ 1.0/rng.Next(1, 100),2) }) .ToArray(); } } //Models: public class ExchangeRateItem { public string FromCurrency { get; set; } public string ToCurrency { get; set; } public double Value { get; set; } } 

Em seguida, no lado frontal, crie um novo componente que receberá
e exibir dados sobre taxas de câmbio do controlador recém-criado.


 ng generate component ExchangeRate -t=true -s=true --skipTests=true 

O conteúdo do componente deve ficar assim:


Código
 import { Component, OnInit, Input } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, Subject } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Component({ selector: 'app-exchange-rate', template: ` <div class="alert alert-danger" *ngIf="errorMessage | async as msg"> {{msg}} </div> <table class='table table-striped'> <thead> <tr> <th>From currency</th> <th>To currency</th> <th>Rate</th> </tr> </thead> <tbody> <tr *ngFor="let rate of rates | async"> <td>{{ rate.fromCurrency }} </td> <td>{{ rate.toCurrency }}</td> <td>{{ rate.value }}</td> </tr> </tbody> </table> `, styles: [] }) export class ExchangeRateComponent implements OnInit { public rates: Observable<ExchangeRateItem[]>; public errorMessage: Subject<string>; @Input() public apiUrl: string; constructor(private http: HttpClient) { this.errorMessage = new Subject<string>(); } ngOnInit() { this.rates = this.http.get<ExchangeRateItem[]>("/api/"+this.apiUrl).pipe(catchError(this.handleError(this.errorMessage)) ); } private handleError(subject: Subject<string>): (te:any) => Observable<ExchangeRateItem[]> { return (error) => { let message = ''; if (error.error instanceof ErrorEvent) { message = `Error: ${error.error.message}`; } else { message = `Error Code: ${error.status}\nMessage: ${error.message}`; } subject.next(message); let emptyResult: ExchangeRateItem[] = []; return of(emptyResult); } } } interface ExchangeRateItem { fromCurrency: string; toCurrency: string; value: number; } 

Agora resta começar a usá-lo na página de dados do aplicativo, simplesmente no modelo, escrevendo a linha <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> e você pode iniciar o projeto novamente. Veremos ao navegar no caminho de destino que o componente recebeu os dados e os exibiu em uma tabela.


Em seguida, tentaremos adicionar uma solicitação de autorização de acesso à API do controlador. Para fazer isso, adicione o atributo [Authorize] na classe ExchangeRateController e execute o SPA novamente, no entanto, depois de voltarmos ao componente que chama nossa API, veremos um erro indicando cabeçalhos de autorização.



Para adicionar corretamente um token de autorização às solicitações de saída, você pode
Engate o mecanismo interceptor angular interceptores. Felizmente, o módulo importado já contém o tipo necessário, basta registrá-lo no módulo base do aplicativo.


 providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true } ], 

Após essas etapas, tudo deve funcionar corretamente. Se você olhar as ferramentas do desenvolvedor novamente, o navegador verá o novo cabeçalho de autorização do Bearer access_token. No back-end, esse token será validado pelo IdentityServer e também dará permissão para chamar um ponto de API seguro.


No final do exemplo de integração com o servidor de autorização, você pode colocar o Activation Guard na rota com os dados da taxa de câmbio no SPA, impedindo que os usuários alternem para a página se não estiverem autorizados no momento. Esse protetor também é apresentado no módulo importado anteriormente, você só precisa pendurá-lo na rota de destino.


 { path: 'data', component: DataComponent, canActivate: [AuthorizeGuard] } 

Agora, no caso em que o usuário não efetuou login no aplicativo e selecionou um link para nosso componente protegido, ele será imediatamente transferido para a página de autorização com uma solicitação para inserir um login e senha. O código resultante está disponível no github .


Conectando um login externo através de um provedor do Google


Existe um pacote Microsoft.AspNetCore.Authentication.Google Nuget separado para conectar o logon através de contas do Google para o ASP.NET core 1.1 / 2.0 +, no entanto, devido a alterações na política da empresa, a Microsoft planeja o ASP.NET Core 3.0+ reconhecê-lo como obsoleto . E agora é recomendável conectar-se por meio do método auxiliar OpenIdConnectExtensions e AddOpenIdConnect , que usaremos neste artigo.


Instale a extensão OpenIdConnect:


 dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7 

Para começar, precisamos obter dois valores-chave do Google - Id Client e Client Secret, para isso, propõe-se executar as seguintes etapas:



Secret Manager . , .


 dotnet user-secrets init dotnet user-secrets set "Authentication:Google:ClientId" "  ClientID" dotnet user-secrets set "Authentication:Google:ClientSecret" "  ClientSecret" 

Google.


 services.AddAuthentication() .AddOpenIdConnect("Google", "Google", o => { IConfigurationSection googleAuthNSection = Configuration.GetSection("Authentication:Google"); o.ClientId = googleAuthNSection["ClientId"]; o.ClientSecret = googleAuthNSection["ClientSecret"]; o.Authority = "https://accounts.google.com"; o.ResponseType = OpenIdConnectResponseType.Code; o.CallbackPath = "/signin-google"; }) .AddIdentityServerJwt(); 

,
Google. , , SPA .



, , OAuth , . Nuget .


Windows


, SPA Microsoft, ActiveDirectory. , Html ASP.NET, WebForms .., Windows WindowsIdentity, , . Identity Server, Windows, claims id_token access_token . , IS4 , ,
github . , ASP.NET Core Identity 3.0.


Identity, Razor Login ExternalLogin ( CLI aspnet-codegenerator ):


 dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet aspnet-codegenerator identity -dc IdentityServer4WebApp.Data.ApplicationDbContext --files "Account.Login;Account.ExternalLogin" 

, Area Identity , .


, . , Identity I AuthenticationSchemeProvider. GetAllSchemesAsync() DisplayName != null, Windows DisplayName = null. LoginModel OnGetAsync :


 ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); // << >> ExternalLogins =(await _schemeProvider.GetAllSchemesAsync()).Where(x => x.DisplayName != null ||(x.Name.Equals(IISDefaults.AuthenticationScheme,StringComparison.OrdinalIgnoreCase))).ToList(); 

private readonly AuthenticationSchemeProvider _schemeProvider . View Login.cshtml :


 <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> << >> <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @(provider.DisplayName ??provider.Name) account">@(provider.DisplayName ??provider.Name)</button> 

, windows launchSettings.json
( IIS, web.config ).


 "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:15479", "sslPort": 44301 } }, 

«Windows» .



SPA IdentityServer . «» [AllowAnonymous] LoginModel [Authorize(AuthenticationSchemes = "Windows")] , , WindowsIdentity.


ExternalLogin , Identity Windows . ProcessWindowsLoginAsync .


 private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl) { var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme); if (result?.Principal is WindowsPrincipal wp) { var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); var props = _signInManager.ConfigureExternalAuthenticationProperties(IISDefaults.AuthenticationScheme, redirectUrl); props.Items["scheme"] = IISDefaults.AuthenticationScheme; var id = new ClaimsIdentity(IISDefaults.AuthenticationScheme); id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); id.AddClaim(new Claim(ClaimTypes.NameIdentifier, wp.Identity.Name)); var wi = wp.Identity as WindowsIdentity; var groups = wi.Groups.Translate(typeof(NTAccount)); var hasUsersGroup = groups.Any(i => i.Value.Contains(@"BUILTIN\Users", StringComparison.OrdinalIgnoreCase)); id.AddClaim(new Claim("hasUsersGroup", hasUsersGroup.ToString())); await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props); return Redirect(props.RedirectUri); } return Challenge(IISDefaults.AuthenticationScheme); } 

, .


ExternalLoginModel.OnPost :


 if (IISDefaults.AuthenticationScheme == provider) { return await ProcessWindowsLoginAsync(returnUrl); } 

Claim Windows Claim «hasUsersGroup», ID access, . ASP.NET Identity UserClaims. ExternalLoginModel .


 private async Task UpdateClaims(ExternalLoginInfo info, ApplicationUser user, params string[] claimTypes) { if (claimTypes == null) { return; } var claimTypesHash = new HashSet<string>(claimTypes); var claims = (await _userManager.GetClaimsAsync(user)).Where(c => claimTypesHash.Contains(c.Type)).ToList(); await _userManager.RemoveClaimsAsync(user, claims); foreach (var claimType in claimTypes) { if (info.Principal.HasClaim(c => c.Type == claimType)) { claims = info.Principal.FindAll(claimType).ToList(); await _userManager.AddClaimsAsync(user, claims); } } } 

OnPostConfirmationAsync (
).


 result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); await UpdateClaims(info, user, "hasUsersGroup");// return LocalRedirect(returnUrl); } 

OnGetCallbackAsync , .


 var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); if (result.Succeeded) { var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); await UpdateClaims(info, user, "hasUsersGroup");// 

, WebAPI
«hasUsersGroup». «ShouldHasUsersGroup»


 services.AddAuthorization(options => { options.AddPolicy("ShouldHasUsersGroup", policy => { policy.RequireClaim("hasUsersGroup");}); }); 

ExchangeRateController
Policy.


  [Authorize(Policy = "ShouldHasUsersGroup")] [HttpGet("api/internalrates")] public IEnumerable<ExchangeRateItem> GetInternalRates() { return Get().Select(i=>{i.Value=Math.Round(i.Value-0.02,2);return i;}); } 

view .


 ng generate component InternalData -t=true -s=true --skipTests=true 

template .


 //internal-data.component.ts: template: `<app-exchange-rate apiUrl="internalrates"></app-exchange-rate> `, //app-routing.module.ts: { path: ' internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard] } //app.component.html: <li class="nav-item" [routerLinkActive]='["link-active"]'> <a class="nav-link text-dark" [routerLink]='["/internaldata"]'>Internal api data</a> </li> 


, , . , accsee_token
claim hasUsersGroup ,
ApiResources . , , appsettings.json , Startup. ConfigureServices .


 services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => { var apiResource = options.ApiResources.First(); apiResource.UserClaims = new[] { "hasUsersGroup" }; }); 

, , windows , .


, – Guard claim «hasUsersGroup» « ». Guard :


 ng generate guard AuthorizeWindowsGroupGuard --skipTests=true 

:


 import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map,tap} from 'rxjs/operators'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; import { AuthorizeService } from "./authorize.service"; import { ApplicationPaths, QueryParameterNames } from './api-authorization.constants'; @Injectable({ providedIn: 'root' }) export class AuthorizeWindowsGroupGuardGuard implements CanActivate{ constructor(private authorize: AuthorizeService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { return this.authorize.getUser().pipe(map((u: any) => !!u && !!u.hasUsersGroup)).pipe(tap((isAuthorized:boolean) => this.handleAuthorization(isAuthorized, state)));; } private handleAuthorization(isAuthenticated: boolean, state: RouterStateSnapshot) { if (!isAuthenticated) { window.location.href = "/Identity/Account/Login?" + QueryParameterNames.ReturnUrl + "=/"; } } } 

, , .


 { path: 'internaldata', component: InternalDataComponent, canActivate: [AuthorizeWindowsGroupGuardGuard] 

IdentityServer, claims ( sub , profile ), «hasUsersGroup». IdentityResource, - IdentityResources Startup.ConfigureServices .


  var identityResource = new IdentityResource { Name = "customprofile", DisplayName = "Custom profile", UserClaims = new[] { "hasUsersGroup" }, }; identityResource.Properties.Add(ApplicationProfilesPropertyNames.Clients, "*"); options.IdentityResources.Add(identityResource); 

- , windows « » SPA – , , Guard .


Conclusão


, ASP.NET Core 3.0 IdentityServer4, . preview , . , github .

Source: https://habr.com/ru/post/pt461433/


All Articles