Uso de Identity Server 4 en Net Core 3.0

Introduccion


Uno de mis proyectos respaldados recientemente enfrentó la tarea de analizar la posibilidad de migrar de .NET framework 4.5 a .Net Core en caso de que sea necesario refactorizar y acumular una gran cantidad de deuda técnica acumulada. La elección recayó en la plataforma de destino .NET Core 3.0, ya que, según los desarrolladores de Microsoft, con el lanzamiento de la versión 3.0, los pasos necesarios para migrar el código heredado disminuirán varias veces. Especialmente nos atrajeron los planes de salida de EntityFramework 6.3 para .Net Core, es decir La mayor parte del código basado en EF 6.2 se puede dejar "tal cual" en un proyecto migrado en el núcleo de la red.


Parece que con el nivel de datos quedó claro, sin embargo, otra gran parte de la transferencia de código fue el nivel de seguridad, que, desafortunadamente, después de una auditoría rápida, tendrá que desecharlo casi por completo y volver a escribirlo desde cero. Afortunadamente, el proyecto ya usaba parte de ASP NET Identity, en forma de almacenar usuarios y otras "bicicletas" adjuntas a un lado.


Esto plantea la pregunta lógica: si la parte de seguridad tiene que hacer muchos cambios, ¿por qué no implementar de inmediato los enfoques recomendados en forma de estándares de la industria, a saber: llevar la aplicación a Open Id connect y OAuth usando el marco IdentityServer4 ?


Problemas y soluciones.


Entonces, se nos ha dado: hay una aplicación de JavaScript en Angular (Cliente en términos de IS4), utiliza un cierto subconjunto de WebAPI (Recursos), también hay una base de datos de Identidad ASP NET desactualizada con inicios de sesión de usuarios que deben reutilizarse después de la actualización (para no comenzar a todos los demás) veces), además, en algunos casos es necesario dar la oportunidad de iniciar sesión en el sistema a través de la autenticación de Windows en el lado de IdentityServer4. Es decir Hay momentos en que los usuarios trabajan a través de una red de área local en un dominio ActiveDirectory.


La solución principal para migrar los datos del usuario es escribir manualmente (o usar herramientas automatizadas) escribir un script de migración entre el antiguo y el nuevo esquema de datos de identidad. Nosotros, a su vez, utilizamos la aplicación de comparación de esquemas de datos automatizados y generamos un script SQL, dependiendo de la versión de Identity, el script de migración de destino contendrá diferentes instrucciones de actualización. Lo principal aquí es no olvidar coordinar la tabla EFMigrationsHistory si EF se usó antes y está planeado en el futuro, por ejemplo, para expandir la entidad IdentityUser a campos adicionales.


Pero ahora se describirá cómo configurar correctamente IdentityServer4 y configurarlo junto con las cuentas de Windows.


Plan de implementación


Por razones de NDA, no describiré cómo logramos implementar IS4 en nuestro proyecto, sin embargo, en este artículo le mostraré en un simple sitio ASP.NET Core creado desde cero qué pasos debe seguir para obtener una aplicación totalmente configurada y funcional que utiliza IdentityServer4 para fines de autorización y autenticación.
Para realizar el comportamiento deseado, debemos seguir los siguientes pasos:


  • Cree un proyecto ASP.Net Core vacío y configúrelo para usar IdentityServer4.
  • Agregar un cliente como una aplicación angular.
  • Inicie sesión a través de open-id-connect google
  • Agregar opción de autenticación de Windows

Por razones de brevedad, los tres componentes (IdentityServer, WebAPI, Angular client) estarán en el mismo proyecto. El tipo de interacción seleccionado entre el cliente y el IdentityServer (GrantType) es el flujo implícito, cuando access_token se pasa al lado de la aplicación en el navegador, y luego se usa al interactuar con WebAPI. Más cerca del lanzamiento, a juzgar por los cambios en el repositorio de ASP.NET Core, el flujo implícito será reemplazado por el Código de autorización + PKCE).


En el proceso de creación y modificación de la aplicación, la interfaz de línea de comandos de .NET Core se utilizará ampliamente, debe instalarse en el sistema en el lugar con la última versión de la versión preliminar de Core 3.0 (al momento de escribir el artículo 3.0.100-preview7-012821).


Creación y configuración de un proyecto web.


El lanzamiento de IdentityServer versión 4 estuvo marcado por el corte completo de la interfaz de usuario de este marco. Ahora los desarrolladores tienen todo el derecho de determinar la interfaz principal del servidor de autorización. Hay varias formas Uno de los más populares es usar la interfaz de usuario del paquete QuickStart UI, que se puede encontrar en el repositorio oficial en github .


Otra forma, no menos conveniente, es la integración con la interfaz de usuario de ASP NET Core Identity, en este caso, el desarrollador necesita configurar correctamente el middleware correspondiente en el proyecto. Este método se describirá más adelante.


Comencemos creando un proyecto web simple. Para hacer esto, ejecute las siguientes instrucciones en la línea de comando:


dotnet new webapp -n IdentityServer4WebApp 

Después de la ejecución, el resultado será un marco de aplicación web, que se llevará gradualmente al estado que necesitamos. Aquí debe hacer una reserva de que .Net Core 3.0 para Identity utiliza RazorPages más ligeros, a diferencia del MVC de peso pesado.
Ahora necesita agregar soporte IdentityServer a nuestro proyecto. Para hacer esto, instale los paquetes necesarios:


 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 

Además de los enlaces a los paquetes del servidor de autorización, aquí hemos agregado soporte de Entity Framework para almacenar información del usuario en el ecosistema de Identidad. Para simplificar, usaremos la base de datos SQLite.


Para inicializar la base de datos, cree nuestro modelo de usuario y contexto de base de datos, para esto declaramos dos clases ApplicationUser, heredadas de IdentityUser en la carpeta Modelos y ApplicationDbContext , heredadas de: ApiAuthorizationDbContext en la carpeta Datos.

A continuación, debe configurar el uso del contexto EntityFramework y crear la base de datos. Para hacer esto, escribimos el contexto en el método ConfigureServices de la clase Startup:


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

Y agregue la cadena de conexión a appsettings.json


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

Ahora puede crear la migración inicial e inicializar el esquema de la base de datos. Vale la pena señalar que se requiere la herramienta instalada para ef core (para la vista previa en cuestión, se necesita la versión 3.0.0-preview7.19362.6).


 dotnet ef migrations add Init dotnet ef database update 

Si todos los pasos anteriores se completaron sin errores, el archivo de datos SQLite data.db debería aparecer en su proyecto.


En esta etapa, podemos configurar y probar completamente la capacidad completa de usar Asp.Net Core Identity. Para hacer esto, realice cambios en los métodos de Inicio. Configure y Startup.ConfigureServices .


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

Con estas líneas, incorporamos la posibilidad de autenticación y autorización en el proceso de procesamiento de solicitudes. Y también agregue la interfaz de usuario predeterminada para Identity.
Solo resta arreglar la IU, agregar a las Páginas \ Compartidas una nueva vista de Razor con el nombre _LoginPartial.cshtml y los siguientes contenidos:


 @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> 

El código de presentación anterior debe agregar enlaces al área de la interfaz de Identidad con controles de usuario integrados en el panel de navegación (inicio de sesión y contraseña, registro, etc.)


Para lograr la representación de nuevos elementos de menú, simplemente modificamos el archivo _Layout.cshtml agregando la representación de esta vista 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> 

Y ahora intentemos ejecutar nuestra aplicación y haga clic en los enlaces que aparecen en la cabeza
menú, el usuario debe ver una página de bienvenida y solicitud
Ingrese nombre de usuario y contraseña. En este caso, puede registrarse e iniciar sesión, todo
Debería funcionar.



Los desarrolladores de IdentityServer4 han hecho un excelente trabajo al mejorar la integración de ASP.NET Identity y el propio marco del servidor. Para agregar la capacidad de usar tokens OAuth2, debe complementar nuestro proyecto con algunas instrucciones nuevas en el código.


En la penúltima línea del método Startup.ConfigureServices, agregue la configuración de convenciones IS4 sobre ASP.NET Core Identity:


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

El método AddApiAuthorization da instrucciones al marco para que use una configuración compatible específica, principalmente a través del archivo appsettings.json. Por el momento, las capacidades de administración de IS4 incorporadas no son tan flexibles y deben considerarse como un punto de partida para crear sus aplicaciones. En cualquier caso, puede usar la versión sobrecargada de este método y configurar los parámetros con más detalle mediante la devolución de llamada.


A continuación, llamamos al método auxiliar, que configura la aplicación para verificar los tokens JWT emitidos por el marco.


 services.AddAuthentication() .AddIdentityServerJwt(); 

Finalmente, en el método Startup.Configure, agregue middleware para
Proporcionar puntos de conexión de Open ID Connect


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

Como se mencionó anteriormente, los métodos auxiliares utilizados leen la configuración en
archivo de configuración de la aplicación appsettings.json , en el que debemos agregar un nuevo
Sección IdentityServer .


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

En esta sección, un cliente se define con el nombre TestIdentityAngular, que asignaremos al futuro cliente del navegador y un perfil de configuración específico.


Application Profiles es una nueva herramienta de configuración de IdentityServer que proporciona varias configuraciones predefinidas con la capacidad de refinar ciertos parámetros. Utilizaremos el perfil IdentityServerSPA , diseñado para casos en que el cliente del navegador y el marco se encuentran en el mismo proyecto y tienen los siguientes parámetros:


  • El recurso redirect_uri establecido en / autenticación / login-callback .
  • Recurso post_logout_redirect_uri, establecido en / autenticación / logout-callback.
  • El conjunto de áreas incluye openid, perfil, para cada recurso API de aplicación.
  • Conjunto de tipos de respuesta OIDC permitidos - token id_token
  • GrantType para el cliente: implícito

Otros posibles perfiles son SPA (aplicación sin IS4), IdentityServerJwt (API compartida con IS4), API (API separada).


Además, la configuración registra recursos:


  • ApiResources: un recurso de API llamado << appname >> API con propiedades para todos los clientes (*).
  • IdentityServerResources: IdentityResources.OpenId () e IdentityResources.Profile ()

Como sabe, IdentityServer usa certificados para firmar tokens, sus parámetros también se pueden establecer en el archivo de configuración, por lo que en el momento de la prueba podemos usar
certificado de prueba x509, para esto debe especificarlo en la sección "Clave" del archivo appsettings.Development.json .


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

Ahora podemos decir que el backend que le permite usar IdentityServer está listo y puede comenzar a implementar la aplicación del navegador.


Implementación angular del cliente


Nuestro SPA basado en navegador se escribirá en la plataforma Angular. La aplicación contendrá dos páginas, una para usuarios no autorizados y otra para usuarios autenticados. Los ejemplos usan la versión 8.1.2


Primero, cree el marco futuro:


 ng new ClientApp 

En el proceso de creación, debe responder "sí" a la propuesta para utilizar el enrutamiento. Y estiliza un poco la página a través de la biblioteca bootstrap:


 cd ClientApp ng add bootstrap 

A continuación, debe agregar soporte de alojamiento SPA a nuestra aplicación principal. Primero debe corregir el proyecto csproj: agregue información sobre nuestra aplicación 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> 

Después de eso, instale el paquete nuget especial para admitir aplicaciones de navegador.


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

Y usamos sus 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"); } }); 

Además de llamar a nuevos métodos, debe eliminar las páginas Razor Index.chtml y _ViewStart.chtml para que los servicios de SPA ahora proporcionen el contenido.


Si todo se hizo de acuerdo con las instrucciones, cuando se inicie la aplicación, aparecerá la página predeterminada en la pantalla.



Ahora necesita configurar el enrutamiento, para esto agregamos al proyecto 2
componente:


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

Los escribimos en la tabla de enrutamiento:


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

Y modificamos el archivo app.component.html para mostrar correctamente los elementos del menú.


 <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> 

En este paso, puede completar la preparación básica del marco de la aplicación para implementar la interacción a través de tokens emitidos por IdentityServer.


La etapa actual de preparación del marco de nuestro SPA se puede llamar completada y ahora deberíamos comenzar a implementar el módulo responsable de interactuar con la parte del servidor utilizando los protocolos OpenID Connect y OAuth. Afortunadamente, los desarrolladores de Microsoft ya han implementado dicho código, y ahora simplemente puede tomar prestado este módulo de ellos. Dado que mi artículo está escrito en base a ASP.NET Core 3.0 pre-lanzamiento 7, tomaremos todo el código usando la etiqueta de lanzamiento "v3.0.0-preview7.19365.7" en github .


Antes de importar el código, debe instalar la biblioteca oidc-client , que
proporciona muchas interfaces para aplicaciones de navegador, así como
admite la gestión de sesiones de usuario y tokens de acceso. Para
Para comenzar a trabajar con él, debe instalar el paquete apropiado.


 npm install oidc-client@1.8.0 

Ahora en nuestro SPA es necesario implementar un módulo que encapsule la interacción completa de acuerdo con los protocolos requeridos. Para hacer esto, debe tomar todo el módulo ApiAuthorizationModule de la etiqueta del repositorio ASP.NET Core anterior y agregar todos sus archivos a la aplicación.


Además, debe importarlo al módulo principal de la aplicación AppModule:


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

Para mostrar nuevos elementos de menú en el módulo importado, hay un componente de menú de inicio de sesión de aplicación ,
se puede cambiar por completo para satisfacer sus necesidades y agregar
un enlace a él en la sección de navegación de la vista app.component.html .


El módulo de autorización API para configurar la conexión OpenID del cliente SPA debe usar un punto final especial en el backend de la aplicación, para su implementación nosotros
debe seguir estos pasos:


  1. Corrija la ID del cliente de acuerdo con lo que establecemos en el archivo de configuración appsettings.json en la sección IdentityServer: Clientes, en nuestro caso es TestIdentityAngular, está escrito en la primera línea del conjunto constante api -autorization.constants.ts.
  2. Agregue un controlador OidcConfigurationController que devolverá directamente la configuración a la aplicación del navegador

El código del controlador creado se presenta a continuación:


  [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); } } 

También debe configurar el soporte de API de puntos para la aplicación de fondo.


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

Ahora es el momento de iniciar la aplicación. Deben aparecer dos elementos en la página principal en el menú superior: Iniciar sesión y Registrarse . Además, al inicio, el módulo de autorización importado solicitará del lado del servidor la configuración del cliente, que posteriormente se tendrá en cuenta en el protocolo. A continuación se muestra un ejemplo de salida de configuración:


 { "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 puede ver, el cliente durante la interacción espera recibir el token de identificación y el token de acceso, y también está configurado para el área de acceso a nuestra API.


Ahora, si seleccionamos el elemento del menú Inicio de sesión , deberíamos redirigirnos a la página de nuestro IdentityServer4 y aquí podemos ingresar el nombre de usuario y la contraseña, y si son correctos, seremos transferidos de inmediato a la aplicación del navegador, que a su vez recibirá id_token y access_token . Como puede ver a continuación, el componente de menú de inicio de sesión de la aplicación determinó que la autorización se completó correctamente y mostró un "saludo", así como un botón para Cerrar sesión .



Cuando abre las "herramientas de desarrollador" en el navegador, puede ver en el backstage toda la interacción utilizando el protocolo OIDC / OAuth. Esto está obteniendo información del servidor de autorización
mediante el punto final .well-conocido / openid-configuration y la actividad de sesión de agrupación a través del punto de acceso connect / checkession. Además, el módulo de autorización está configurado para el mecanismo de "actualización silenciosa de tokens", cuando el token de acceso caduca, el sistema pasa independientemente los pasos de autorización en un iframe oculto. Puede deshabilitar las actualizaciones automáticas de tokens estableciendo el valor del parámetro includeIdTokenInSilentRenew en "falso" en el archivo authorize.service.ts .


Ahora puede ocuparse de restringir el acceso a usuarios no autorizados desde los componentes de la aplicación SPA, así como algunos controladores de API en la parte posterior. Para demostrar algunas API, crearemos una clase ExchangeRateItem en la carpeta Modelos , así como un controlador en la carpeta Controlador que devuelve algunos datos aleatorios.


 //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; } } 

Luego, en el lado frontal, cree un nuevo componente que recibirá
y mostrar datos sobre los tipos de cambio del controlador recién creado.


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

El contenido del componente debería verse así:


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; } 

Ahora queda por comenzar a usarlo en la página de datos de la aplicación, simplemente en la plantilla escribiendo la línea <app-exchange-rate apiUrl = "rates"> </app-exchange-rate> y puede comenzar el proyecto nuevamente. Cuando navegamos a lo largo de la ruta de destino, veremos que el componente recibió los datos y los mostró en forma de tabla.


A continuación, intentaremos agregar una solicitud de autorización de acceso a la API del controlador. Para hacer esto, agregue el atributo [Autorizar] en la clase ExchangeRateController y ejecute SPA nuevamente, sin embargo, después de volver al componente que llama a nuestra API, veremos un error que indica encabezados de autorización.



Para agregar correctamente un token de autorización a las solicitudes salientes, puede
Active el mecanismo interceptor de interceptores angulares. Afortunadamente, el módulo importado ya contiene el tipo necesario, solo necesitamos registrarlo en el módulo base de la aplicación.


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

Después de estos pasos, todo debería funcionar correctamente. Si vuelve a mirar las herramientas del desarrollador, el navegador verá el nuevo encabezado de autorización Bearer access_token. En el backend, este token será validado por IdentityServer y también dará permiso para llamar a un punto API seguro.


Al final del ejemplo de integración con el servidor de autorizaciones, puede colocar el Activation Guard en la ruta con los datos del tipo de cambio en el SPA, evitará que los usuarios cambien a la página si no están autorizados actualmente. Este protector también se presenta en el módulo importado anteriormente, solo necesita colgarlo en la ruta de destino.


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

Ahora, en el caso de que el usuario no haya iniciado sesión en la aplicación y haya seleccionado un enlace a nuestro componente protegido, será redirigido inmediatamente a la página de autorización con una solicitud para ingresar un nombre de usuario y contraseña. El código resultante está disponible en github .


Conexión de un inicio de sesión externo a través de un proveedor de Google


Existe un paquete separado de Microsoft.AspNetCore.Authentication.Google Nuget para conectar el inicio de sesión a través de cuentas de Google para ASP.NET core 1.1 / 2.0 +, sin embargo, debido a cambios en la política de la propia corporación, Microsoft tiene planes para ASP.NET Core 3.0+ Reconocerlo como obsoleto . Y ahora se recomienda conectarse a través del método auxiliar OpenIdConnectExtensions y AddOpenIdConnect , que usaremos en este artículo.


Instale la extensión OpenIdConnect:


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

Para comenzar, necesitamos obtener dos valores clave de Google: Id Client y Client Secret, para esto se propone realizar los siguientes pasos:


  • Siga el enlace y seleccione el botón "CONFIGURAR UN PROYECTO".
  • En el cuadro de diálogo que aparece, especifique el servidor web.
  • A continuación, en el cuadro de texto "URI de redireccionamiento autorizado", especifique el punto final en el lado de la aplicación para el redireccionamiento de autorización de Google (ya que ahora el sitio se encuentra en https: // localhost: 44301 / , luego especificamos https: // localhost: 44301 /signin-google )
  • ClientID Client Secret.
  • , , URL .

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 .


Conclusión


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

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


All Articles