6:49 0 0
Login personalizado para Blazor Webasembly

Login personalizado para Blazor Webasembly

  DrUalcman |  octubre 182020

Estoy pasandome a Blazor, concretamente a su modo Webasembly y me encanta. Pero como no deja de ser codigo C# estoy pudiendo importar muchas cosas ya hechas en otros proyectos. Y una de ella es el tema de la identificación de usuarios personalizada. Estas líneas van dedicadas a rodos aquellos que no nos gusta utilizar herramientas de terceros, o que sencillamente, tenemos que mantener códigos antiguos, y no soportan las nuevas funcionalidades de identificación de usiarios incorporadas por el framework.

Hoy sencillamente vamos a refactorizar un poco lo que ya habiamos hecho para NET CORE en este otro blog. Por lo que te recomiendo su lectura. Recordar que este sistema de autenticación está baso den Cookies, por lo que la información se guarda de forma local en el PC del usuario y tienen un tiempo determinado.

Preparando el sistema

Blazor webasembly es una aplicación que corre 100% en la máquina del cliente, por lo que para identificar al usuario de forma correcta y segura, siempre dependerá de una API que controle al usuario del lado del servidor. Pero no por ello es exclusibo. Si lo deseas puedes programar un sistema que los nombres de usuario esten a mano en el código y así la aplicación podría ser 100% ofline para identicar al usuario. Pero entonces este post no te serviría de mucho, ya que tu identificación sería sólo en el lado del lciente. Por ello empecemos por preparar la API y configurarla para autenticación con cookies. Para ello te remito a mi este otro blog de nuevo.

Preparando la App

Como siempre digo, habrá muchas formas de hacer esto mismo, pero esta es mi solución, bueno que no es sólo mía, es aplicar lo aprendido en este curso de Blazor Webasembly, de recomendada lectura, por lo que vamos a seguir un poco los pasos que allí se describen.

Crear el Modelo que almacenara los datos del usuario

En mi caso yo he llamado a mi modelo Clientes, y maneja una tabla de la base de datos con el mismo nombre desde Entity Framwork. He aqui mi modelo:

public class Clientes
{
public int IdCliente { get; set; }
[Required(ErrorMessage = "What is your name please?")]
public string Nombre { get; set; }
public string Nick { get; set; }
[Required]
[MaxLength(16, ErrorMessage = "Password too long (16 character limit).")]
[MinLength(8, ErrorMessage = "Password too short (8 character min).")]
public string Password { get; set; } = string.Empty;
[Required(ErrorMessage = "What is your email please?")]
public string Email1 { get; set; }
public string Img { get; set; }
public int IdUsuario { get; set; }
public int IdLocal { get; set; }
public int IdEmpresa { get; set; }
public bool Activado { get; set; }
public bool Visible { get; set; }

public Clientes()
{
this.Nombre = string.Empty;
this.Nick = string.Empty;
this.Psswrd = string.Empty;
this.Email1 = string.Empty;
this.Img = "Ninguna";
this.IdCliente = 0;
this.IdUsuario = 0;
this.Activado = false;
this.Visible = false;
this.IdLocal = 5;
this.IdEmpresa = 0;
}
}

Ahora una pequeña clase para controlar si el usuario está autenticado o no.

    public class UserInfo
{
public bool IsAuthenticated { get; set; }
public Clientes User { get; set; }
}

Crear nuestro manejador de Identidad

En éste momento es dónde vamos a refactorizar un poco lo que se ve en el blog the autenticación para NET CORE. La clase Identity la vamos a convertir en estática, ya que todas las funciones qeu va a tener van a ser ejecutadas directamente pasandole solo objetos. La gran refactorización viene de hacer que reciba un parámetro T para poder recibir cualquier tipo de clase modelo. Además de agregarle un poco más de funcionalidad.

Como vamos a recibir un parámetro T debemos de inicializarlos como new() cuando se declara la clase, para así poder crear el objeto nuevo cuando sea preciso. Como no soy muy bueno explicando, mejor os pongo el código y analizarlo un poco. Luego comentaré un par de detalles.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Reflection;
using System.Collections.Generic;

namespace BlazorClassLibrary.Security
{
///
/// Manage the actions to Sign In and Sign Out users using Personalized Model Claims
/// Default authentication type is CookieAuthenticationDefaults.AuthenticationScheme
///

///
public static class Identity where T: new()
{
#region login actions
///
/// Login request fro a model user send
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

///
///
///
public static async Task SignInAsync(HttpContext context, T user)
{
return await SignInAsync(context, user, CookieAuthenticationDefaults.AuthenticationScheme);
}

///
/// Login request fro a model user send
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

/// Who is the property to show default like user name in the Identity
///
///
///
public static async Task SignInAsync(string displayName, HttpContext context, T user)
{
return await SignInAsync(context, user, CookieAuthenticationDefaults.AuthenticationScheme, displayName);
}


///
/// Login request fro a model user send
///

///
///
/// Set what authentication will used
///
public static async Task SignInAsync(HttpContext context, T user, string authenticationType)
{
return await SignInAsync(context, user, CookieAuthenticationDefaults.AuthenticationScheme, "name");
}

///
/// Login request fro a model user send
///

///
///
/// Set what authentication will used
/// Who is the property to show default like user name in the Identity
///
public static async Task SignInAsync(HttpContext context, T user, string authenticationType, string displayName)
{
bool result;
try
{
await context.SignInAsync(authenticationType,
new ClaimsPrincipal(SetUserData(user, authenticationType, displayName)),
new AuthenticationProperties
{
IsPersistent = false,
ExpiresUtc = DateTime.UtcNow.AddMinutes(60)
});
result = true;
}
catch
{
result = false;
}
return result;
}

///
/// Sign out the user
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

///
///
public static async Task SignOutAsync(HttpContext context)
{
await SignOutAsync(context, CookieAuthenticationDefaults.AuthenticationScheme);
}

///
/// Sing out the user
///

///
/// Set what authentication will used
///
public static async Task SignOutAsync(HttpContext context, string authenticationType)
{
await context.SignOutAsync(authenticationType);
}
#endregion

#region user calls
///
/// Get the claims about the active user and return the user model
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

///
///
public static async Task GetUserAsync(HttpContext context)
{
return await GetUserAsync(context, CookieAuthenticationDefaults.AuthenticationScheme);
}

///
/// Get the claims about the active user and return the user model
///

///
/// Set what authentication will used
///
public static async Task GetUserAsync(HttpContext context, string authenticationType)
{
try
{
T User = new T();
AuthenticateResult x = await context.AuthenticateAsync(authenticationType);
if (x.Succeeded)
{
//use reflexion to fill the object
Type myType = typeof(T);
string model = $"{myType.Namespace}.{myType.Name}";
//Type t = Assembly.GetExecutingAssembly().GetType(model, true, true);

//Get assemblies loaded in the current AppDomain
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

int c = 0;
do
{
if (assemblies[c].GetName().Name == myType.Namespace)
{
Type t = assemblies[c].GetType(model, true, true);
foreach (Claim item in x.Principal.Claims)
{
if (item.Type != ClaimTypes.Name)
{
PropertyInfo property = t.GetProperty(item.Type);
//convert to the correct property type
Type tipo = property.PropertyType;
if (!tipo.IsEnum) property.SetValue(User, Convert.ChangeType(item.Value, tipo));
else property.SetValue(User, item.Value);
}
}
c = assemblies.Length;
}
c++;
} while (c < assemblies.Length);


}
return User;
}
catch (Exception ex)
{
throw new Exception("Get user claims exception", ex);
}
}

///
/// Set the claims for the user
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

///
///
public static ClaimsIdentity SetUserData(T user)
{
return SetUserData(user, CookieAuthenticationDefaults.AuthenticationScheme);
}

///
/// Set the claims for the user
/// Authentication used CookieAuthenticationDefaults.AuthenticationScheme
///

/// Who is the property to show default like user name in the Identity
///
///
public static ClaimsIdentity SetUserData(string displayName, T user)
{
return SetUserData(user, CookieAuthenticationDefaults.AuthenticationScheme, displayName);
}

///
/// Set the claims for the user
///

///
/// Set what authentication will used
///
public static ClaimsIdentity SetUserData(T user, string authenticationType)
{
return SetUserData(user, authenticationType, "name");
}


///
/// Set the claims for the user
///

///
/// Set what authentication will used
/// Who is the property to show default like user name in the Identity
///
public static ClaimsIdentity SetUserData(T user, string authenticationType, string displayName)
{
List claims = new List();
//use reflexion to get dynamic the properties about the user object
PropertyInfo[] properties = typeof(T).GetProperties();
bool foundName = false;
foreach (PropertyInfo property in properties)
{
//get property name
string myType = property.Name;
//get value of the property
string myValue = property.GetValue(user).ToString();
claims.Add(new Claim(myType, myValue));
if (myType.ToLower() == displayName.ToLower())
{
claims.Add(new Claim(ClaimTypes.Name, myValue));
foundName = true;
}
}
if (!foundName)
{
//not found DisplayName property, search a email
Claim n = claims.Find(n => n.Value.Contains("@"));
if (n != null) claims.Add(new Claim(ClaimTypes.Name, n.Value));
else //if nothing found set the first property like display name
claims.Add(new Claim(ClaimTypes.Name, claims[0].Value));
}
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, authenticationType);
return claimsIdentity;
}
#endregion
}
}
Método SetUserData

En éste método es dónde decodificamos mediante Refelxion el objeto con el modelo recibido y lo almacenamos como ClaimsIdentity. Interesando es saber que cuando se llama a la propiedad User.Identity.Name siempre espera por defecto una Claim del tipo ClaimTypes.Name o lo que es lo mismo, una Claim del tipo http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name. Es por ello que éste método espera un parámetro displayName que si no es encontrado busca algún correo electrónico dentro de todas las claims. Si no encuentra nada entonces mostrará el primer elemento de las claims. Es básicamente para que no quede como vacío si intentamos llamar e User.Identity.NAme. Si no os gusta con quitarlo teneis.

Médito GetUserAsync

Aquí recuperamos la información de la Cookie con las Claims del usuario, y como estamos almacenando todos los datos del usuario no necesitamos leer la base de datos de nuevo. Este es uno de los usos que le podemos dar a este tipo de autenticación. Pero aqui viene otra parte de la refactorización, Y es que como estamos recibiendo un parámetro de tipo T tenemos que buscar su clase dentro de la aplicación. Esto reduce un poco el rendimiendo de la aplicación, tema a considerar si lo que buscamos es un rendimiendo super rápido.

Para no tener que realizar ésta acción la clase con los datos del usuario debe de estar en el mismo projecto que la clase Identity y entonces pasar directametne la ruta completa de la case dentro de la librería, como se muestra en el blog anterior.

Crear en la API una nueva acción para controlar el estado del usuario

En nuestra API, tenemos que crear una acción dentro de algún controlador, normalmente el mismo que utilizaremos para identificar al usuario, para conocer el estado del usuario.

        private static UserInfo LoggedOutUser = new UserInfo { IsAuthenticated = false, User = new Clientes() };

[HttpGet("account/user")]
public async Task GetUser()
{
Clientes user = await Identity.GetUserAsync(this.HttpContext);
return User.Identity.IsAuthenticated ? new UserInfo { IsAuthenticated = true, User = user ?? new Clientes() } : LoggedOutUser;
}

Como puedes observar, ya estamos haciendo uso de nuesto objeto Identity para recuperar los datos de usuario desde la Cookie y devolveremos un objero UserInfo con los datos del usuario. Todo esto sin necesidad de hacer uso de la base de datos, y así librar de trabajo un poco al los servidores.

Crear un AuthenticationStateProvider

Ahora pasaremos a nuestra app en Blazor para preparar que comprueb si el susuario está autenticado del lado del servidor y para ello vamos a crear una clase para controlar el estado de la autenticación que va a heredar de Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider

public class ServerAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient httpClient;
public ServerAuthenticationStateProvider(HttpClient client)
{
this.httpClient = client;
}
public override async Task GetAuthenticationStateAsync()
{
//check is the user are authenticated
UserInfo user = await this.httpClient.GetFromJsonAsync("account/user");
ClaimsIdentity identity = user.IsAuthenticated ?
Identity.SetUserData(user.User) :
new ClaimsIdentity(); //user is not authenticated
return new AuthenticationState(new ClaimsPrincipal(identity));
}
}

En esta clase ya se hace uso, de nuevo, objeto Identity que hemos creado en el paso anterior para establecer las ClaimsIdentity del usuario que está autenticado. Y está llamando a la API para obtener los datos del usuario. De este modo podemos saber si el usuario está identificado o no del lado del servidor.

Configurar el cliente para autenticación

Llegó el momento de configurar nuestra aplicación Blazor para autenticar al usuario del lado del cliente. En el archivo Program.cs necesitamos configurar 3 servicios:

  1. Services.AddOptions();
  2. Services.AddAuthizationCore();
  3. Services.AddScopedServerAuthenticationStateProvider>();

Los dos primeros son los necesarios para la autenticación del usuario. El tercero es el manejador de estado que hemos preparado en el paso anterior. Por lo que nuestro archovo debe quedar algo asi.

    public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
//prepare the client for the injection in more than one service
HttpClient Client = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) };
//start the app
builder.RootComponents.Add("app");
builder.Services.AddTransient(sp => Client);
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped(); //register our authentication model
//register our full default dependency
builder.Services.AddScoped();
await builder.Build().RunAsync();
}
}

Por último las acciones de inicio de sesión y cerrar sesión

Aunque realmente este paso lo podríamos haber puesto dentro del paso donde modigicamos la API, lo he dejado para el final por seguir con la lógica de los pasos. Aqui no hay mucho cambio que hacer, básicamente vamos a crear dos acciones, las cuales van a hacer uso de nuevo de nuestor objeto Identity que es el encargado de registrar las Cookies del lado del cliente correctamente, o de elimnarlas. Por lo que directamente vamos al código

        [HttpPost("account/signin")]
public async Task> SignIn(Clientes user)
{
//auth user
Clientes loggedInUser = await this.DbContext.Clientes
.Where(c => c.Nick == user.Nick && c.Password == user.Password)
.FirstOrDefaultAsync();

if (loggedInUser != null)
{
return await Identity.SignInAsync(this.HttpContext, loggedInUser);
}
else return false;
}

[HttpGet("account/signout")]
public async Task SignOut()
{
await Identity.SignOutAsync(this.HttpContext);
return Redirect("~/");
}

No hay mucho que comentar. Una vez que hemos validado el usuario es cuando enviamos el objeto a nuestra clase Identity para registrar las Cookies con las Claims del usuario. Para 
cerrar sesión lo mismo pero para eliminarlas.

Voy a ver si hago un projecto de ejemplo solo con esto, pero por ahora no tengo nada, así que espero que con estas líneas sea suficiente para que entendais el proceso. Como siempre me encantaría ver los comentarios con las mejoras o dudas para que todos aprendamos un poco más sobre este marabilloso mundo de la programación y ahora en Blazor.


0 Comentarios

 
 
 

Archivo