Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 25, 2022 03:32 am GMT

ASP.NET Core: Servidor de Autenticacin con OpenID Connect

Introduccin

En este post (un poco largo) veremos los conceptos principales de OpenID Connect y una implementacin de ejemplo en ASP.NET Core.

Para la implementacin de OpenID Connect en .NET utilizaremos OpenIddict-core y .NET 6.

El ejemplo completo lo puedes ver en mi GitHub, te recomiendo que lo clones para una mejor comprensin del cdigo que veremos aqu.

Puedes seguirme en Twitter en @balunatic donde suelo poner cosas de programacin, pero si tienes dudas o cualquier cosa, manda DM .

OpenID Connect Protocol

Qu es OpenID Connect (OIDC)?

OpenID Connect (o OIDC) es un protocolo de identidad que utiliza los mecanismos de autenticacin y autorizacin de OAuth 2.0. La especificacin final de OIDC fue publicada en Febrero del 2014 y al da de hoy ha sido adoptado por una gran cantidad de proveedores de identidad.

OAuth 2.0 es un protocolo de autorizacin y OIDC es un protocolo de autenticacin y es usado usado para verificar la identidad de un usuario en un servicio tercero (conocido como Relying Party).

Es decir, cuando haces una aplicacin web y permites que tus usuarios inicien sesin con sus cuentas actuales como de Google o Facebook, es cuando se usa OIDC.

Tu aplicacin web es ese Relying Party y esta aplicacin no debe de tener las credenciales del usuario de Google o Facebook, ya que estamos buscando no exponer esa informacin y gracias OIDC no es necesario que las manipulemos. Siguiendo el flujo correcto, OIDC ayuda autenticar a un usuario sin que se tenga que crear una nueva cuenta en tu aplicacin web y reutilizando cuentas existentes de servicios populares (de nuevo, como google o facebook).

Una gran variedad de clientes pueden usar OpenID Connect para autenticar usuarios, desde Single Page Applications (SPAs como Angular o React) hasta aplicaciones mviles nativas. Tambin es usado en Single Sign On en muchas aplicaciones (SSO es muy til cuando en tu organizacin tienen una gran variedad de servicios o aplicaciones internas y es necesario usar una misma cuenta).

Diferencias entre OAuth 2.0 y OIDC

Realmente OIDC es funcionalidad adicional a la que ya existe en OAuth 2.0. Mientras OAuth 2.0 se trata del mecanismo para autorizar el acceso a informacin, OIDC se centra en la identidad del usuario (su autenticacin).

El propsito principal de OIDC es darte un solo lugar donde tengas que iniciar sesin sin importar la cantidad de sitios que tengas. Es decir, cuando tu accedes a tu aplicacin que requiere un usuario autenticado, eres mandado a tu servidor de identidad (que utiliza OpenID) y ah inicias sesin, eres redirigido de vuelta a la aplicacin donde empezaste pero ya como un usuario autenticado. Las credenciales y tu informacin personal la tiene el proveedor de identidad y las aplicaciones de terceros solo necesitan saber si eres quien dices ser.

Sin embargo, OAuth se enfoca en proteger informacin y restringir el acceso. Por ejemplo: Cuando entras a myapp.com e inicias sesin con Facebook, se utiliza OpenID. Pero cuando en myapp.com te pregunta Quieres importar tus contactos de Facebook a myapp.com? ah se utilizara OAuth, ya que Facebook te preguntar Permitir a myapp.com que acceda a tu listado de contactos? y ese consentimiento es llevado a cabo siguiendo las reglas de OAuth.

Elige el flujo correcto de autenticacin

Algo que no he terminado de mencionar, es que OAuth y OIDC cuentan con varias formas de autenticar usuarios y es importante saber sus diferencias, porque dependiendo de la aplicacin cliente, se utilizar un flujo diferente.

Flujos no interactivos

Los flujos no interactivos no requieren que el usuario interacte con el Authorization Server.

Resource owner password (no recomendado para aplicaciones nuevas)

Este flujo es directamente inspirado en el basic autentication (donde las credenciales se mandan en un header codificadas en base 64). En este caso, es el flujo ms simple que existe en la especificacin OAuth 2.0: La aplicacin cliente (el Relying party) pregunta el usuario/contrasea, enva una solicitud de autorizacin al identity provider e inmediatamente regresa un access token si este es autorizado.

Image description

Nota : Este flujo no se recomienda ya que el usuario y contrasea son directamente expuestos en la aplicacin cliente. Por esta razn, este flujo no debe de ser usado cuando aplicaciones de terceros se ven involucrados. Si estas desarrollando aplicaciones internas (tu tienes el control de todo), por simplicidad, puede funcionar.

Client credentials grant (recomendado para comunicacin machine-2-machine)

Este flujo podra decirse que es idntico al anterior (resource owner password) pero este est diseado para comunicacin machine-2-machine (es decir, un servicio hablando con otro servicio) y ningn usuario se involucra en este flujo.

La aplicacin cliente solicita el token enviando las credenciales y si estas son correctas, obtiene su access_token para acceder a los servicios requeridos.

Image description

Flujos interactivos

Estos flujos, como su nombre lo indica, requieren de interaccin del usuario con el servidor de autenticacin. De esta forma, las credenciales del usuario solo son usadas en el servidor de autenticacin y ningn tercero las tiene que manipular.

Authorization code flow (recomendado para aplicaciones nuevas)

Este flujo probablemente es el ms complicado de todos, ya que involucra redirecciones del navegador y comunicacin con el backend. Sin embargo, es el ms recomendado para cualquier escenario que involucre usuarios finales, ya que podran iniciar sesin con sus credenciales, un PIN, un Smart Card o incluso usando otro proveedor externo.

Como ventaja de esta complejidad, el access_token jams pasa por el navegador, ya que el backend hace ese intercambio de informacin directamente con el servidor de autenticacin una vez que el usuario fue verificado.

Bsicamente hay 2 pasos importantes en este flujo: La solicitud y respuesta al endpoint authorization y lo mismo con el endpoint token.

Image description

Authorization request

En este flujo, la aplicacin cliente inicia el proceso de autenticacin generando una solicitud de autorizacin incluyendo siempre el parmetro response_type=code, el client_id, el redirect_uri y opcionalmente, un scope y state (este ltimo ayudando mitigar la vulnerabilidad de ataques XSRF).

Si la solicitud es vlida, el servidor pedir al usuario que se autentique y generalmente se pedir el consentimiento de compartir informacin con la aplicacin cliente (aunque esto depende directamente de la implementacin del authorization server).

Image description

Si se inicia sesin y se da permiso, el navegador (user-agent) redirige de vuelta a la aplicacin cliente incluyendo como parmetro en el URL un authorization code (este es un pequeo token, nico y con una vida muy corta) y este se usa nicamente para intercambiar el cdigo por un access_token y id_token.

Token request

Cuando la aplicacin obtiene el authorization code, debe inmediatamente intercambiarlo por el access token y as dar por finalizado el proceso de autenticacin.

Esto suena un proceso complicado, pero realmente siempre es igual, as que existen libreras o frameworks completos que ya hacen esto por nosotros.

Implicit flow

Implicit flow es muy similar al authorization code, excepto que no existe ese intercambio entre authorization code y los tokens (en el token request): el access token es directamente regresado al cliente como parte del proceso de autorizacin, es decir, en el redirect que existe desde el servidor de autorizacin y aplicacin cliente, los tokens forman parte del URI.

Image description

Este flujo tpicamente es usado en aplicaciones frontend que no tienen backend que pueda hacer el intercambio del cdigo recibido por el token.

Este flujo es menos seguro, porque los access token viajan por medio de un fragmento del URI y estos no se encuentran encriptados ni protegidos de ninguna forma.

Existen formas de prevenir ser vulnerable, pero la mejor opcin es usar el flujo anterior utilizando el Proof Key for Code Exchange.

Existen ms flujos que no es necesario aprender.

Realmente, los importantes son client credentials y authorization code flow y por simplicidad y si todo es interno, resource owner password. Existen otros como hybrid flow, device flow, etc. pero varios son obsoletos o no tan usados.

La siguiente tabla nos ayudar mejor a decidir:

Type of ApplicationOAuth 2.0 flow
Server-side (AKA Web)Authorization Code flow
SPAAuthorization Code flow with PKCE o Implicit flow (solo si no hay compatibilidad en el navegador para usar Crypto Web)
NativeAuthorization Code flow with PKCE
TrustedResource Owner Password flow
ServiceClient Credentials

Implementando un Servidor de autenticacin en ASP.NET Core

Acaso creen que ya habamos terminado? Seguimos ahora con el cdigo.

En este post veremos como crear 3 aplicaciones web: Servidor de autenticacin (Identity Provider con OpenID), Aplicacin web Cliente (El Relying Party que necesita de usuarios autenticados y acceder a recursos protegidos) y una Web API (resource protegido del usuario al que la aplicacin web cliente quiere acceder).

Proyecto IdentityServer: AKA Authorization Server

Para esto, necesitamos una aplicacin web que nos administre usuarios. Para dejarlo simple, utilizaremos el template de asp.net core web con individual credentials que usa Identity Core:

dotnet new razor --auth individual --use-local-db true -o IdentityServer

Al usar cuentas individuales se utilizar Identity Core y el parmetro local-db simplemente indica que queremos usar SQLServer en lugar de SQLite (que es el default).

Nota : Para mejor referencia, puedes ver mi repositorio de GitHub y ver este ejemplo completo.

Para este authorization server vamos a usar una muy buena librera llamada Openiddict (una solucin ms simple que Identity Server de Duente, pero completamente libre de usar).

Instalamos OpenIddict agregando sus paquetes:

<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.1" /><PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" /><PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.1" /><PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" /><PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1" /><!-- OpenIddict --><PackageReference Include="OpenIddict" Version="3.0.0" /><PackageReference Include="OpenIddict.AspNetCore" Version="3.0.0" /><PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0" /><!-- /OpenIddict -->

y comenzamos a configurar el servidor desde el Program.cs.

Hay que configurar OpenIddict para que use el ApplicationDbContext que por default nos agreg el template.

using IdentityServer.Data;using Microsoft.AspNetCore.Identity;using Microsoft.EntityFrameworkCore;using OpenIddict.Abstractions;var builder = WebApplication.CreateBuilder(args);var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");builder.Services.AddDbContext<ApplicationDbContext>(options =>{    options.UseSqlServer(connectionString);    // Register the entity sets needed by OpenIddict.    options.UseOpenIddict();});

OpenIddict guardar en SQLServer (usando EF Core) toda la informacin que necesita para aplicaciones clientes y sus tokens emitidos.

Despus de eso, agregamos las dependencias de OpenIddict y los Flows permitidos:

builder.Services.AddOpenIddict()    // Register the OpenIddict core components.    .AddCore(options =>    {        // Configure OpenIddict to use the EF Core stores/models.        options            .UseEntityFrameworkCore()            .UseDbContext<ApplicationDbContext>();    })    // Register the OpenIddict server components.    .AddServer(options =>    {        options            .AllowClientCredentialsFlow()            .AllowAuthorizationCodeFlow()            .RequireProofKeyForCodeExchange()            .AllowRefreshTokenFlow();        options            .SetTokenEndpointUris("/connect/token")            .SetAuthorizationEndpointUris("/connect/authorize")            .SetUserinfoEndpointUris("/connect/userinfo");        // Encryption and signing of tokens        options            .AddEphemeralEncryptionKey()            .AddEphemeralSigningKey()            .DisableAccessTokenEncryption();        // Register scopes (permissions)        options.RegisterScopes("api");        options.RegisterScopes("profile");        // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.        options            .UseAspNetCore()            .EnableTokenEndpointPassthrough()            .EnableAuthorizationEndpointPassthrough()            .EnableUserinfoEndpointPassthrough();    });

Aqu estamos haciendo lo siguiente:

  • Estamos permitiendo el flujo de autorizacin client credentials y authorization code, cualquier flujo que se intente usar diferente, la solicitud ser rechazada
    • Tambin se est habilitando PKCE y de hecho se hace obligatorio implementarlo (por que es ms seguro, especialmente en SPAs).
    • Tambin se habilita la posibilidad de usar Refresh Tokens.
  • Se estn estableciendo las rutas para los endpoints:
    • Token. Para intercambiar el authorization code por access tokens y id tokens.
    • Authorization. Para solicitar el cdigo de autorizacin despus de haber iniciado sesin.
    • User Info. Para solicitar informacin adicional del usuario una vez autenticado.
  • Con AddEphemeralEncryptionKey y AddEphemeralSigninKey se genera una llave RSA asimtrica para modo desarrollo, ya que no se guarda en ningn lado y no se comparte entre instancias.
    • Nota: cada vez que reiniciar el el servidor, se generan llaves nuevas. Solo se usa para testing.
  • Se registran los scopes que se usarn, estos funcionan como permisos.
  • Los mtodos Passthrough son para que nosotros podamos tomar accin de esos endpoints despus de ser validados por OpenIddict.

Para finalizar con el contenido del Program, contina con el siguiente cdigo boilerplate.

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)    .AddEntityFrameworkStores<ApplicationDbContext>();builder.Services.AddRazorPages();builder.Services.AddControllers();builder.Services.AddDatabaseDeveloperPageExceptionFilter();var app = builder.Build();// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){    app.UseMigrationsEndPoint();}else{    app.UseExceptionHandler("/Error");    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.    app.UseHsts();}app.UseHttpsRedirection();app.UseStaticFiles();app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.MapRazorPages();app.MapControllers();await SeedDefaultClients();app.Run();

Inicialmente, necesitamos poder crear clientes de OpenID, por eso tenemos el mtodo Seed que se describe a continuacin:

async Task SeedDefaultClients(){    using var scope = app.Services.CreateScope();    var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();    var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();    await context.Database.EnsureCreatedAsync();    var client = await manager.FindByClientIdAsync("clientwebapp");    if (client is null)    {        await manager.CreateAsync(new OpenIddictApplicationDescriptor        {            ClientId = "clientwebapp",            ClientSecret = "client-web-app-secret",            DisplayName = "ClientWebApp",            RedirectUris = { new Uri("https://localhost:7003/signin-oidc") },            Permissions =            {                OpenIddictConstants.Permissions.Endpoints.Authorization,                OpenIddictConstants.Permissions.Endpoints.Token,                OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,                OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,                OpenIddictConstants.Permissions.GrantTypes.RefreshToken,                OpenIddictConstants.Permissions.Prefixes.Scope + "api",                OpenIddictConstants.Permissions.Prefixes.Scope + "profile",                OpenIddictConstants.Permissions.ResponseTypes.Code            }        });    }}

Este Seed nos crea un cliente llamado clientwebapp y aqu mismo se especifican los permisos que tiene y su secret (que es necesario para validaciones futuras).

Habitualmente esto se podra hacer desde una UI, para dar de alta o de baja los clientes que desees usar, pero dejemos esto como un simple demo.

El template ya contiene UI para crear usuarios (registro) e inicio de sesin, todo utilizando la implementacin default de Identity Core, por lo que esta parte no tenemos que implementar algo.

Lo que s tenemos que hacer, es generar los claims (segn el usuario autenticado) y validar las solicitudes que se reciben.

Para esto, crearemos el siguiente controller:

using Microsoft.AspNetCore;using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Mvc;using OpenIddict.Abstractions;using OpenIddict.Server.AspNetCore;using System.Security.Claims;namespace IdentityServer.Controllers;public class AuthorizationController : Controller{    [HttpGet("~/connect/authorize")]    [HttpPost("~/connect/authorize")]    [IgnoreAntiforgeryToken]    public async Task<IActionResult> Authorize()    {        var request = HttpContext.GetOpenIddictServerRequest() ??                      throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");        // Retrieve the user principal stored in the authentication cookie.        var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);        // If the user principal can't be extracted, redirect the user to the login page.        if (!result.Succeeded)        {            return Challenge(                authenticationSchemes: IdentityConstants.ApplicationScheme,                properties: new AuthenticationProperties                {                    RedirectUri = Request.PathBase + Request.Path + QueryString.Create(                        Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())                });        }        // Create a new claims principal        var claims = new List<Claim>            {                // 'subject' claim which is required                new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),                new Claim(OpenIddictConstants.Claims.Username, result.Principal.Identity.Name),                new Claim(OpenIddictConstants.Claims.Audience, "test"),            };        var email = result.Principal.Claims.FirstOrDefault(q => q.Type == ClaimTypes.Email);        if (email is not null)        {            claims.Add(new Claim(OpenIddictConstants.Claims.Email, email.Value));        }        var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);        // Set requested scopes (this is not done automatically)        claimsPrincipal.SetScopes(request.GetScopes());        foreach (var claim in claimsPrincipal.Claims)        {            claim.SetDestinations(claim.Type switch            {                // If the "profile" scope was granted, allow the "name" claim to be                // added to the access and identity tokens derived from the principal.                OpenIddictConstants.Claims.Name when claimsPrincipal.HasScope(OpenIddictConstants.Scopes.Profile) => new[]                {                    OpenIddictConstants.Destinations.AccessToken,                    OpenIddictConstants.Destinations.IdentityToken                },                // Never add the "secret_value" claim to access or identity tokens.                // In this case, it will only be added to authorization codes,                // refresh tokens and user/device codes, that are always encrypted.                "secret_value" => Array.Empty<string>(),                // Otherwise, add the claim to the access tokens only.                _ => new[]                {                    OpenIddictConstants.Destinations.AccessToken                }            });        }        // Signing in with the OpenIddict authentiction scheme trigger OpenIddict to issue a code (which can be exchanged for an access token)        return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);    }    [HttpPost("~/connect/token")]    public async Task<IActionResult> Exchange()    {        var request = HttpContext.GetOpenIddictServerRequest() ??                      throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");        ClaimsPrincipal claimsPrincipal;        if (request.IsClientCredentialsGrantType())        {            // Note: the client credentials are automatically validated by OpenIddict:            // if client_id or client_secret are invalid, this action won't be invoked.            var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);            // Subject (sub) is a required field, we use the client id as the subject identifier here.            identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId ?? throw new InvalidOperationException());            // Add some claim, don't forget to add destination otherwise it won't be added to the access token.            identity.AddClaim("some-claim", "some-value", OpenIddictConstants.Destinations.AccessToken);            claimsPrincipal = new ClaimsPrincipal(identity);            claimsPrincipal.SetScopes(request.GetScopes());        }        else if (request.IsAuthorizationCodeGrantType())        {            // Retrieve the claims principal stored in the authorization code            claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;        }        else if (request.IsRefreshTokenGrantType())        {            // Retrieve the claims principal stored in the refresh token.            claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;        }        else        {            throw new InvalidOperationException("The specified grant type is not supported.");        }        // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.        return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);    }    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]    [HttpGet("~/connect/userinfo")]    public async Task<IActionResult> Userinfo()    {        var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;        return Ok(new        {            Sub = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),            Name = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),            Occupation = "Developer",            Age = 31        });    }}

Te recomiendo que leas el cdigo, investigues tus dudas y lo depures paso a paso para entender mejor el funcionamiento, pero aqu te va un resumen:

Authorize

Este endpoint es el punto de entrada cuando se quiere autenticar. Con GetOpenIddictServerRequestse lee la solicitud OpenID y en caso de no ser vlida, se lanza la excepcin.

Si es vlida, se verifica si el usuario actualmente est autenticado, sino, se manda a autenticar utilizando el inicio de sesin que nos ofrece Identity Core. El esquema de Cookies que agrega Identity Core es IdentityConstants.ApplicationScheme y ese es el esquema que usamos para mandarlo a iniciar sesin.

Una vez que inicia sesin, el RedirectUri lo regresa de vuelta a este mtodo Authorize() y ahora el flujo continuar, generando los claims que desees, porque ya tenemos un usuario autenticado.

Ha este punto OpenIddict ya valid la solicitud y valid los scopes, por eso pasamos a generar los claims y poner su destino.

El destino es importante, ya que el access_token es el que ser usado por terceros, no debe de contener informacin delicada, justo como los comentarios lo describen.

Exchange

Este mtodo, como su nombre lo dice, sirve para intercambiar un authorization code por los tokens (tanto access y identity) en caso de que el flujo aplique.

En este ejemplo tambin se incluye el flujo clients credential, que solo se necesita el client_id y el client_secret (hablando del ejemplo machine-2-machine) para generar los tokens. Solo est ah como referencias, si deseas hacer pruebas con este flow.

User Info

Sinceramente, este es el que menos he probado, pero sirve para proveer informacin adicional al momento de que un usuario autorizado regrese a la aplicacin cliente, ms adelante veremos como decirle a ASP.NET que consulte tambin este endpoint despus de un inicio de sesin exitoso.

Para qu sirve todo esto?

Lo que acabamos de hacer fue configurar nuestro Identity provider, si nos vamos al URL https://localhost:7001/.well-known/openid-configuration (los puertos pueden variar) obtenemos la siguiente configuracin:

{  "issuer": "https://localhost:7001/",  "authorization_endpoint": "https://localhost:7001/connect/authorize",  "token_endpoint": "https://localhost:7001/connect/token",  "userinfo_endpoint": "https://localhost:7001/connect/userinfo",  "jwks_uri": "https://localhost:7001/.well-known/jwks",  "grant_types_supported": [    "client_credentials",    "authorization_code",    "refresh_token"  ],  "response_types_supported": [    "code"  ],  "response_modes_supported": [    "form_post",    "fragment",    "query"  ],  "scopes_supported": [    "openid",    "offline_access",    "api",    "profile"  ],  "claims_supported": [    "aud",    "exp",    "iat",    "iss",    "sub"  ],  "id_token_signing_alg_values_supported": [    "RS256"  ],  "code_challenge_methods_supported": [    "S256"  ],  "subject_types_supported": [    "public"  ],  "token_endpoint_auth_methods_supported": [    "client_secret_basic",    "client_secret_post"  ],  "claims_parameter_supported": false,  "request_parameter_supported": false,  "request_uri_parameter_supported": false}

Este endpoint sirve para explorar el Identity Provider, conocer sus endpoints, sus llaves pblicas para las firmas de los tokens, los claims soportados, los scopes y tipos de flujos que soporta.

Este endpoint es usado por las aplicaciones clientes que utilizaran este servidor como su proveedor de identidad.

Lo que sigue ahora, es crear esa aplicacin cliente.

Proyecto WebClient: Aplicacin Web Cliente.

Aqu crearemos una aplicacin Razor de la misma forma que lo hicimos con el anterior, pero este, sin autenticacin ni nada ms:

dotnet new razor -o WebClient

Nota : Los URLs usados en la configuracin del cliente es importante, ya que forma parte de las validaciones y si este no es correcto, el proceso de autorizacin ser rechazado.

Para este proyecto necesitaremos el siguiente paquete:

<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.1" />

Esta aplicacin de Razor pages trae el template default con bootstrap pero sin autenticacin. Lo que vamos a hacer en el Program.cs es lo siguiente:

using Microsoft.IdentityModel.Protocols.OpenIdConnect;var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddRazorPages();builder.Services.AddAuthentication(options =>{    options.DefaultScheme = "Cookies";    options.DefaultChallengeScheme = "oidc";}).AddCookie("Cookies", options =>{    options.Cookie.Name = ".ClientWebAppAuth";}).AddOpenIdConnect("oidc", options =>{    options.Authority = "https://localhost:7001";    options.ClientId = "clientwebapp";    options.ClientSecret = "client-web-app-secret";    options.ResponseType = OpenIdConnectResponseType.Code;    options.Scope.Add("api");    options.Scope.Add("openid");    options.Scope.Add("profile");    options.SaveTokens = true;    options.GetClaimsFromUserInfoEndpoint = true;    options.TokenValidationParameters.NameClaimType = "name";});var app = builder.Build();// Configure the HTTP request pipeline.if (!app.Environment.IsDevelopment()){    app.UseExceptionHandler("/Error");    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.    app.UseHsts();}app.UseHttpsRedirection();app.UseStaticFiles();app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.MapRazorPages()    .RequireAuthorization();app.Run();

Hay dos cosas importantes aqu, pero este es el resumen:

  • AddAuthentication. Establece los esquemas de autenticacin que por default se usarn en la aplicacin.
  • AddCookie. Agrega el esquema de autenticacin por cookie, esto significa que una vez autenticado en este esquema, se generar una cookie de autenticacin llamada .ClientWebAppAuth.
  • AddOpenIdConnect. Aqu configuramos OpenID Connect y le estamos indicando que use el servidor de autenticacin previamente creado:
    • Authority. Es el host del servidor de autenticacin
    • ClientId, Client secret y Response type. Esta informacin debe de coincidir con el cliente creado por el Seed. Por ahora, solo permitimos response_type=code ya que en el proceso de autorizacin solo vamos a regresar el authorization code, para que este sea usado inmediatamente como ya lo vimos.
    • Scope. Aqu estamos agregando los scopes que necesitamos, no necesitas siempre todos, pero son los que la aplicacin requiere (solo como ejemplo, se podr no solicitar el scope de profile si no queremos informacin adicional del usuario).
    • SaveTokens. Esto indica que los tokens (access y identity) se guardarn en la cookie de auteneticacin, esto para ser usados despus (ejem. para llamar a la API protegida).
    • GetClaimsFromUserInfoEndpoint: Este flag indica si queremos consultar informacin adicional del usuario, estos se guardarn como claims en la cookie previamente configurada.
    • TokenValidationParameters.NameClaimType: Aqu simplemente le indicamos a ASP.NET el nombre del claim que queremos que use para el nombre. Ejem. cuando usamos User.Identity.Name

Actualizamos Index.cshtml para poder ver los claims que se nos han emitido por el servidor de autorizacin:

@[email protected]@modelIndexModel@{ViewData["Title"]="Homepage";}<divclass="text-center"><h1class="display-4">Welcome</h1><p>Learnabout<ahref="https://docs.microsoft.com/aspnet/core">buildingWebappswithASP.NETCore</a>.</p></div>@if(User.Identity!.IsAuthenticated){<h2>[email protected]</h2><ul>@foreach([email protected]){<li>@claim.Type:@claim.Value</li>}<li>access_token:@(awaitHttpContext.GetTokenAsync("access_token"))</li><li>id_token:@(awaitHttpContext.GetTokenAsync("id_token"))</li></ul>}

Probando la autenticacin

Necesitamos correr el IdentityServer y el WebClient juntos. Al abrir WebClient este automticamente nos redireccionar al IdentityServer para ser autenticados. Una vez finalizando el proceso, regresaremos a WebClient y tendremos el siguiente resultado:

Image description

Es importante mencionar que debemos de generar las migraciones de Entity Framework correspondientes y ejecutarlas (con dotnet ef) en el IdentityServer.

En este caso, como mi IdentityServer no tiene ms informacin ma mas que mi correo, eso se est usando como mi nombre, pero podramos extenderlo y agregar ms informacin (esto, ya tema para ms a rato referente a Identity Core)

Proyecto ProtectedApi: La Web API protegida

Y por ltimo, falta lo que queremos proteger, en este caso, una Web API.

Tericamente, esta Web API contiene informacin privada del usuario autenticado, y para eso necesitamos un access_token para validar que usuarios nos acceden.

Por medio de JWT y Bearer authentication, usaremos los access tokens para validar cada solicitud que llega a nuestra Web API. Pero antes de empezar, hay que crear otro proyecto:

dotnet new web -o ProtectedApi

Para poder usar la autenticacin por medio de JWT en nuestra API, necesitamos el siguiente paquete:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />

El ejemplo ser muy sencillo, tendremos solamente un endpoint en nuestra Web Api que requiere de un usuario autenticado:

var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthentication("Bearer")      .AddJwtBearer("Bearer", options =>      {          options.Authority = "https://localhost:7001";          options.Audience = "IdentityServerWebClients";      });builder.Services.AddAuthorization();var app = builder.Build();app.UseHttpsRedirection();app.UseAuthentication();app.UseAuthorization();app.MapGet("/me", (HttpRequest request) =>{    var user = request.HttpContext.User;    return Results.Ok(new    {        Claims = user.Claims.Select(s => new        {            s.Type,            s.Value        }).ToList(),        user.Identity.Name,        user.Identity.IsAuthenticated,        user.Identity.AuthenticationType    });}).RequireAuthorization();app.Run();

En este caso, el esquema default que se configura para la autenticacin es Bearer. Estamos indicando quien es nuestro Identity Provider (authority) para que nos ayude a autenticar las solicitudes que se reciben.

Aqu ocurre la magia, por que ProtectedApi realmente no sabe nada de llaves privadas o pblicas, pero las necesita para validar los access token que le llegan. En este caso, al indicar el host del authority, este automticamente va y lee el endpoint /.well-known revisado anteriormente y lee las llaves pblicas RSA que se necesitan para verificar los tokens.

WebClient tiene los access token, entonces necesitamos que este mismo intente acceder a ProtectedApi una vez autenticado.

Por lo tanto, agregamos la siguiente Razor page en WebClient llamada Me.cshtml:

using Microsoft.AspNetCore.Authentication;using Microsoft.AspNetCore.Mvc.RazorPages;using System.Net.Http.Headers;namespace WebClient.Pages{    public class MeModel : PageModel    {        private readonly HttpClient _http;        public MeModel(IHttpClientFactory httpClientFactory)        {            _http = httpClientFactory.CreateClient();        }        public string RawJson { get; set; } = default!;        public async Task OnGet()        {            var accessToken = await HttpContext.GetTokenAsync("access_token");            _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);            var response = await _http.GetAsync("https://localhost:7005/me");            response.EnsureSuccessStatusCode();            RawJson = await response.Content.ReadAsStringAsync();        }    }}

Aqu por fines del ejemplo se est haciendo de una forma no muy prctica, pero lo que quiero dejar claro es como hacer una llamada HTTP a una API protegida con el access token otorgado por el Identity Provider (que a su vez, est guardada en el authorization cookie de WebClient).

Si consultamos esta pgina (navegando a https://localhost:7003/me) desplegamos el resultado visualizando el RawJson:

@page@model WebClient.Pages.MeModel<h2>Calling Protected API result:</h2>@Model.RawJson

Image description

Si hacemos llamadas sin autenticar, o un JWT invlido, se regresar un HTTP 401 Unauthorized.

Conclusin

Esto pareciera un tema complicado, pero una vez que haces este ejemplo por ti mismo, puedes ver que s es sencillo, ya que se hace una vez y de ah funciona para autenticar N aplicaciones (agregando sus respectivos clientes).

Extenderlo, agregar ms proveedores o ms tipos de autenticacin (ejemplo SAML) todo se hace en el IdentityServer y no hay nada ms que cambiar en las dems aplicaciones, ya que los clientes hablan OpenID Connect y los claims y tokens estn generados en base eso.

Como tarea, agrega Google o Facebook al IdentityServer para que pueda ser usado como opcin tambin al iniciar sesin en WebClient.

Otra tarea que puedes realizar es agregar un cliente Javascript con backend empleando el patrn BFF (Backend For Frontend) o sin backend utilizando la librera helper oidc-client-js.

Referencias


Original Link: https://dev.to/isaacojeda/aspnet-core-servidor-de-autenticacion-con-openid-connect-59kh

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To