Blog dedicado a la programación .NET y la informática en general

Archivo para la Categoría "ASP.NET"

Autenticación con Membership ASP.NET Identity

La autenticación Membership tradicional de ASP.NET tiene algunas limitaciones que ahora gracias a WIF (Windows Identity Foundation) se mejora. No voy a entrar en detalles técnicos y en definiciones ya que estoy introduciéndome en esta tecnología y todavía no estoy capacitado para ello.

Por tanto la intención de estos apuntes son los de mostrar un ejemplo de cómo integrar este tipo de autenticación en una aplicación web vacía, ya que las plantilla que VS2013 incluye traer otro módulos que no tienen por qué servirnos y por tanto es muy útil saber qué necesitamos para implementarlo desde cero sin depender de esta plantilla. Se da por supuesto que se tiene una base de datos SQL Server donde poder realizar las pruebas.

Lo primero es instalar mediante NuGet los siquitentes paquetes:

  • Microsoft ASP.NET Identity EntityFramework
  • Microsoft ASP.NET Identity Owin
  • Microsoft.Owin.Host.SystemWeb

Captura

El segundo paso es modificar el web.config. Para ello previamente debemos tener la cadena de conexión con la BBDD sobre la que se van a hacer las pruebas. Dicha base de datos no tiene porqué tener previamente las tablas de Membership generadas, sino que el mismo aplicativo cuando intenta acceder a éste las crea de forma automática.

Por tanto se agrega la cadena de conexión y se incluye el tag “sessionState” indicándole la cadena de conexión a la que debe conectarse.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- ... -->
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Investigando;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<!-- ... -->
<sessionState mode="InProc" customProvider="DefaultSessionProvider">
<providers>
<add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DefaultConnection" />
</providers>
</sessionState>
</system.web>
</configuration>

El tercer paso es agregar una clase que asocie la aplicación con el tipo de autenticación. Para ello hay que incluir en la raiz de la aplicación una clase llamada “Starup.cs”.

image

El código a incluir puede ser el siguiente:

using Microsoft.Owin;
using Owin;

namespace WebApplication2
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}

Para que funcione también hay que incluir otra clase dentro de la carpeta “App_Start” de la aplicación web:

image

Y que contenta el siguiente código:

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;

namespace WebApplication2
{
public partial class Startup
{

// Para obtener más información sobre la configuración de la autenticación, visite http://go.microsoft.com/fwlink/?LinkId=301883
public void ConfigureAuth(IAppBuilder app)
{
// Habilitar la aplicación para que use una cookie para almacenar la información del usuario que inició sesión
// y almacenar también información acerca de un usuario que inicie sesión con un proveedor de inicio de sesión de un tercero.
// Es obligatorio si la aplicación permite a los usuarios iniciar sesión
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

// Quitar las marcas de comentario de las líneas siguientes para habilitar el inicio de sesión con proveedores de inicio de sesión de terceros
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");

//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");

//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");

//app.UseGoogleAuthentication();
}
}
}

Se debe tener en cuenta que se ha cambiado el espacio de nombres y se le ha quitado “App_Start”. Y OJO con las rutas que se le están indicando, se supone que luego se van a crear para la autenticación, lo digo por este código –> LoginPath = new PathString(“/Account/Login”).

El cuarto paso va a consistir en comprobar que efectivamente podemos tener acceso a la base de datos y crear usuarios y roles. En este caso se crea una nueva clase para gestionar las identidades. Como se está usando EntityFramework mediante CodeFirst se generará un contexto de acceso, una clase para manejar los usuarios y un gestor de operaciones sobre los mismos. Este código lo podéis sacar de los proyectos plantilla de ASP.NET. El código para gestionar los roles los he obtenido en el siguiente enlace.

La clase quedaría de la siguiente forma:

image

using System;
using WebApplication2.Models;
using System.Collections.Generic;

namespace WebApplication2.Models
{
// Para agregar datos del usuario, agregue más propiedades a su clase de usuario. Visite http://go.microsoft.com/fwlink/?LinkID=317594 para obtener más información.
public class ApplicationUser : IdentityUser
{
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
}

public class UserManager : UserManager<ApplicationUser>
{
public UserManager()
: base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
{
}
}

public class IdentityManager
{
public bool RoleExists(string name)
{
var rm = new RoleManager<IdentityRole>(
new RoleStore<IdentityRole>(new ApplicationDbContext()));
return rm.RoleExists(name);
}

public bool CreateRole(string name)
{
var rm = new RoleManager<IdentityRole>(
new RoleStore<IdentityRole>(new ApplicationDbContext()));
var idResult = rm.Create(new IdentityRole(name));
return idResult.Succeeded;
}

public bool CreateUser(ApplicationUser user, string password)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var idResult = um.Create(user, password);
return idResult.Succeeded;
}

public bool AddUserToRole(string userId, string roleName)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var idResult = um.AddToRole(userId, roleName);
return idResult.Succeeded;
}

public void ClearUserRoles(string userId)
{
var um = new UserManager<ApplicationUser>(
new UserStore<ApplicationUser>(new ApplicationDbContext()));
var user = um.FindById(userId);
var currentRoles = new List<IdentityUserRole>();
currentRoles.AddRange(user.Roles);
foreach (var role in currentRoles)
{
um.RemoveFromRole(userId, role.Role.Name);
}
}
}
}

namespace WebApplication1
{
public static class IdentityHelper
{
// Se utilizan para XSRF al vincular inicios de sesión externos
public const string XsrfKey = "XsrfId";

public static void SignIn(UserManager manager, ApplicationUser user, bool isPersistent)
{
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

public const string ProviderNameKey = "providerName";
public static string GetProviderNameFromRequest(HttpRequest request)
{
return request[ProviderNameKey];
}

public static string GetExternalLoginRedirectUrl(string accountProvider)
{
return "/Account/RegisterExternalLogin?" + ProviderNameKey + "=" + accountProvider;
}

private static bool IsLocalUrl(string url)
{
return !string.IsNullOrEmpty(url) && ((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) || (url.Length > 1 && url[0] == '~' && url[1] == '/'));
}

public static void RedirectToReturnUrl(string returnUrl, HttpResponse response)
{
if (!String.IsNullOrEmpty(returnUrl) && IsLocalUrl(returnUrl))
{
response.Redirect(returnUrl);
}
else
{
response.Redirect("~/");
}
}
}
}

Aquí tengo que hacer hincapié en que dentro del código hay referencias a rutas de autenticación que van a ser desarrolladas supuestamente a posteriori y que no se incluye en este ejemplo.

Por último queda consumir esta última clase que se ha construido y comprobar que se han creado correctamente el usuario administrador y su rol correspondiente.

Se agrega en Global.asax.cs el siguiente código:

protected void Session_Start(object sender, EventArgs e)
{
// Comprueba que hay un rol de administrador y un usuario asociado
IdentityManager identMgr = new IdentityManager();
if (!identMgr.RoleExists(ROLADM))
{
identMgr.CreateRole(ROLADM);
var user = new ApplicationUser() { UserName = "Administrador" };
if (identMgr.CreateUser(user, "c1@v3~"))
{
identMgr.AddUserToRole(user.Id, ROLADM);
}
}
}

Para terminar comprobando que una vez que se lanza la aplicación se han creado las tablas de forma automática y se ha generado el usuario y rol correspondientes:

image

He realizado un video con todo el proceso completo, pero la calidad del mismo deja mucho que desear debido al escaso tiempo que le he podido dedicar. Pero como una imagen vale más que mil palabras os lo dejo a vuestra disposición, ya que no disponemos de mucha información en este sentido en español.

Ejemplo de implementación de Membership con ASP.NET Identity

 

Un saludo.

Anuncios

Como hacer persistente la autenticación en ASP.NET mediante Cookies en Membership

Nota : Me he encontrado entre mis borradores este artículo que no publiqué en su momento, ya está desfasado pero quién sabe si le sirve a alguien por lo que lo publico ahora.

Puede daros el caso como el que yo tengo en el que vuestro proveedor de hosting limite el tiempo máximo de timeout de la sessión a un valor escaso para vuestro fin. Lo cual obliga a vuestros usuarios a tener que autenticarse cada vez que acceden a vuestra web. Para evitar esto y no depender del timeout de sesión podemos hacer persistente la autenticación mediante el uso de Cookies.

Las cookies son ficheros de texto de intercambio entre el explorador y el servidor que almacenan información relevante a la página web y de forma individual para cada usuario. Estos ficheros son almacenados en el equipo que usa el cliente y en algunos casos son objeto de fines malintencionados por lo que evitaremos guardar información confidencial y en nuestro caso almacenaremos un token o símbolo que identifique al usuario.

Cuando el usuario ha sido autenticado correctamente se crea la cookie a guardar en el cliente:

   1: // Crea el contenedor de autenticación
   2: FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
   3:                                                                     this.LoginUser.UserName,
   4:                                                                     DateTime.Now,
   5:                                                                     DateTime.Now.AddDays(Constantes.VariablesConfiguracion.AutenticationCookieDaysExpired),
   6:                                                                     true,
   7:                                                                     string.Empty);
   8:
   9: // Encripta the ticket de autenticación
  10: string encTicket = FormsAuthentication.Encrypt(ticket);
  11:
  12: // Crea la cookie
  13: HttpCookie galleta = new HttpCookie(Constantes.AutenticacionCookieName, encTicket);
  14: galleta.Expires = ticket.Expiration;
  15: Response.Cookies.Add(galleta);

Para recuperar la cookie almacenada seguiremos este otro código:

   1: MembershipUser usuarioAutenticado = Membership.GetUser();
   2:
   3: // Comprueba si tiene una cookie almacenada para restaurar la autenticación
   4: if (usuarioAutenticado == null)
   5: {
   6:     // Se comprueba si hay una cookie almacenada
   7:     HttpCookie myCookie = Request.Cookies[Constantes.AutenticacionCookieName];
   8:
   9:     // Lee la cookie y comprueba el usuario y si es válido lo da como autenticado
  10:     if (myCookie != null)
  11:     {
  12:         FormsAuthenticationTicket ticketRecupeado = FormsAuthentication.Decrypt(myCookie.Value);
  13:         string strUsuario = ticketRecupeado.Name;
  14:         MembershipUser usuarioCookie = Membership.GetUser(strUsuario);
  15:         if (usuarioCookie != null && usuarioCookie.IsApproved && !usuarioCookie.IsLockedOut)
  16:         {
  17:             FormsAuthentication.SetAuthCookie(usuarioCookie.UserName, true);
  18:             Response.Redirect(Request.Url.AbsoluteUri);
  19:         }
  20:     }
  21: }

Y eliminamos la cookie cuando el usuario cierra la sesión manualmente.

   1: // Desconecta al usuario activo y elimina sus cookies
   2: HttpCookie myCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
   3: if (myCookie != null)
   4: {
   5:     myCookie.Expires = DateTime.Now;
   6:     Response.Cookies.Set(myCookie);
   7: }

Espero que os funcione.

Como activar la página de errores de ASP.NET

Primero indicar que esto es una MALA PRÁCTICA, pero a veces se hace necesaria cuando no tienes acceso al servidor de hosting como te gustaría, en mi caso no puedo guardar un fichero log de errores, tampoco en base de datos y de hecho no llega ni a pasar por la página inicial, y de depurar en remoto ni hablamos.

La forma más fácil cuando no sabes el error que te da la aplicación es cambiar el web.config y activar la página web de errores que viene por defecto en ASP.NET.

Para ello poner lo siguiente:

<system.web>
<compilation debug="true" targetFramework="4.5">
<customErrors mode="Off" />
</system.web>

Una vez que tengáis el error volver a ponerlo como estuviera, como repito, es una MALA PRÁCTICA.

En mi caso ha sido porque el hosting ha incluido el Framework 4.5 y ahora las configuraciones del web.config son visibles también en los subdominios. Por lo cual la sección correspondiente a las EnterpriseLibrary se estaban pisando y al ser un error del fichero de configuración no pasaba ni tan siquiera por el Global.asax, así que no tenía forma de capturar el error y gracias a esta modificación que comento he podido comprobar el error y arreglarlo.

Detalles del error:

Tipo del Error: Configuration Error 

Descripción: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Mensaje del Error: Section or group name ‘loggingConfiguration’ is already defined. Updates to this may only occur at the configuration level where it is defined.

Espero que no lo tengáis que utilizar.

Generación de controles web personalizados ASP.NET (WebCustomControl)

Este probablemente sea mi primer mensaje del blog con cierto contenido ya extenso.

La generación de controles personalizados en .NET no es que sea algo precisamente trivial por eso he creado esta entrada en mi blog.

Aquí pretendo dar las indicaciones necesarias para crear un sencillo control web personalizado desde cero y llegar a un nivel que creo aceptable y que abrirá las puertas para llegar a hacer controles realmente profesionales.

Nos iniciaremos primero leyendo lo que MSDN nos indica sobre la clase WebControl, incluso ya puestos, si controlamos algo de inglés podemos ver el siguiente video webcast de Microsoft.

La clase WebControl es la principal clase de la que se debe heredar cuando queremos hacer un control web personalizado.

Trabajaré sobre un control que he tenido que generar para mi trabajo. Se trata de un control que filtra los resultados obtenidos a ser mostrados en un control DataGrid. El lector debe tener en cuenta que el proyecto en el que estaba trabajando comenzó con el Framework 1.1 y que por tanto se utilizan algunos conceptos ya obsoletos en Framework 4.0, pero lo importante es igual de válido para cualquiera de ellos.

Lo primero que haremos desde Visual Studio será agregar un nuevo elemento a nuestro proyecto (entiendo que el lector tiene ciertos conocimientos de programación en .NET y obviaré ciertos detalles) que será un “Control Web personalizado”. La clase resultante nos da una idea de por dónde debemos ir, pero en absoluto deja completo un control web.

La intención del control que he utilizado en este ejemplo es asociarle un control DataGrid (aunque podría haberse hecho perfectamente con un GridView) y que de forma automática lea las columnas que tiene asociadas para agregarlas a un control DropDownList de donde el usuario pueda elegir la columna por la que quiere filtrar la información a mostrar en el control DataGrid. Luego el valor a filtrar se agregará a un control TextBox y podrá indicar si quiere que la búsqueda sea susceptible a mayúsculas y minúsculas. Se realizará el filtrado una vez que haga click en un control Button. Todas las búsquedas sobre las columnas se hará como si fueran texto y mediante la sentencia LIKE ‘*valor*’ que está descrita en la propiedad Expression de la clase DataColumn. Además el control que se genera será el encargado de paginar y ordenar el DataGrid según lo indique el usuario. El origen de datos va a ser un fichero XML con los datos de Personas y que estará definido en un DataSet tipado. Como medio intermedio se agrega una clase preparada para ser consumida por un objeto ObjectDataSource.

Comenzamos la programación definiendo parámetros por defecto

[DefaultProperty("Titulo")]
[DefaultEvent("GetMeDataSource")]
[ToolboxData("<{0}:FiltroDataGrid runat=server></{0}:FiltroDataGrid>")]

public
class FiltroDataGrid : WebControl, INamingContainer

La propiedad por defecto es “Título” aunque no le he encontrado mucha utilidad, y para cuando se haga doble click en el control en tiempo de diseño se le ha añadido el evento por defecto “GetMeDataSource”, que de hecho va a ser el único evento que lance el control. Cabe destacar la herencia de la interfaz “INamingContainer” la cual se encargará que nuestros identificadores especificados en el control no se pisen con los identificadores que contenga la página de por sí. Yo de todas formas me he encargado que los identificadores sean únicos en el código asociándole el identificador del control.

Continuamos especificando las propiedades públicas que va contener el control. Lo único a destacar en este código es que dichas propiedades se guardan en el “ViewState” de la página que contenga el control.

#region Propieades Públicas
/// <summary>
/// Título del filtro
/// </summary>
[Bindable(true)]
[Category("Objetos")]
[DefaultValue("Poner título")]
[Localizable(true)]
public string Titulo
{
    get
    {
        string s = (string)ViewState[this.ID + ".Titulo"];
        return (s ?? string.Empty);
    }

    set
    {
        ViewState[this.ID + ".Titulo"] = value;
    }
}

/// <summary>
/// Identificador del DataGrid asociado para el filtrado
/// </summary>
[Bindable(true)]
[Category("Objetos")]
public string GridName
{
    get
    {
        return ViewState[this.ID + ".GridName"] as string;
    }

    set
    {
        ViewState[this.ID + ".GridName"] = value;
    }
}
#endregion

Ahora toca definir los controles que van a entrar en juego en la funcionalidad del control. En este punto debemos tener en cuenta que los controles deben permanecer como variables globales dentro de la clase, sino no formarán parte del ViewState y será imposible recuperar en la parte del servidor la información que haya sido introducida por el usuario desde la parte cliente.

#region Controles Persistentes
private DropDownList ddlColumna;
private TextBox txtValor;
private Button btnFiltrar;
private CheckBox chkCaseSensitive;
#endregion

Continuamos dibujando los controles en la parte cliente. Cuando se crea la clase WebCustomControl por defecto sólo nos genera el método sobrescrito “RederContents”, que está muy bien para definir código html pero que luego en tiempo de diseño no muestra los controles que introduzcamos en “CreateChildControls”. El código para el renderizado es el siguiente:

#region Métodos de renderizado
protected override void CreateChildControls()
{
    // Listado de columnas
    ddlColumna = new DropDownList();
    ddlColumna.ID = this.ID + "Ddl";
    if (this.Grid != null)
    {
        this.Grid.PageIndexChanged += Grid_PageIndexChanged;
        this.Grid.SortCommand += Grid_SortCommand;
        ddlColumna.Items.Add(new ListItem("- Ninguno -", string.Empty));
        foreach (BoundColumn gridColumn in this.Grid.Columns)
        {
            ddlColumna.Items.Add(new ListItem(gridColumn.HeaderText, gridColumn.DataField));
        }
    }
    this.Controls.Add(ddlColumna);
    // Cuadro de texto para el valor de filtrado
    txtValor = new TextBox();
    txtValor.ID = this.ID + "Txt";
    this.Controls.Add(txtValor);
    // Chek para distinguir entre mayúsculas y minúsculas
    chkCaseSensitive = new CheckBox();
    chkCaseSensitive.Text = "Distinguir may./min.";
    this.Controls.Add(chkCaseSensitive);
    // Botón para actualizar los datos filtrados
    btnFiltrar = new Button();
    btnFiltrar.Text = "Filtrar";
    btnFiltrar.CausesValidation = false;
    btnFiltrar.Click += btnFiltrar_Click;
    this.Controls.Add(btnFiltrar);
    // Llama al método base
    base.CreateChildControls();
}

protected override void RenderContents(HtmlTextWriter output)
{
    // Ejemplo de cómo adaptar el estilo del control al Grid
    string style = "style=\"background:#" + this.Grid.HeaderStyle.BackColor.R.ToString("X") + this.Grid.HeaderStyle.BackColor.G.ToString("X") + this.Grid.HeaderStyle.BackColor.B.ToString("X") + "\"";
    if (this.Grid.Width.Value > 0)
    {
        style = "style=\"background:#" + this.Grid.HeaderStyle.BackColor.R.ToString("X") + this.Grid.HeaderStyle.BackColor.G.ToString("X") + this.Grid.HeaderStyle.BackColor.B.ToString("X") + "; width:" + this.Grid.Width + "\"";
    }
    // Presenta visualmente el contenido del control
    string salida = string.Format("\r\n<div id=\"" + this.ID + "Div\"{1}>{0}<br />\r\n", Titulo, string.IsNullOrEmpty(style) ? string.Empty : style);
    output.Write(salida);
    base.RenderContents(output);
    salida = "\r\n</div>";
    output.Write(salida);
}

protected override void Render(HtmlTextWriter writer)
{
    // De esta forma nos aseguramos que se muestra en tiempo de ejecución
    EnsureChildControls();
    base.Render(writer);
}
#endregion

Como nota al código explicaré que Grid es un objeto que nos devuelve el DataGrid asociado al control mediante la propiedad “GridName”. Lo que hace básicamente es buscar en la página que contiene al control un control con el identificador indicado en la propiedad “GridName”. Ojo porque si la página que lo contiene es una “MasterPage” no será suficiente son buscarlo con el método “FindControl”.

El truco para que se muestre el control tal cual en tiempo de diseño es sobrescribir el método “Render” y usar “EnsureChildControls”, esto se asegura que antes de renderizar el control se han generado todos los controles secundarios del mismo. De otra forma sólo se verá el código html que hayamos descrito en “RenderContents”.

El resto del código es para gestionar los eventos de los controles, paginar, filtrar y ordenar el DataGrid y lanzar un evento para que se vuelvan a pasar los datos que mostrará el “DataGrid” en su propiedad “DataSource”.

Creo que hasta aquí lo explicado es suficiente para su entendimiento pero si alguien se queda con ganas de más o tiene dudas al respecto, gustosamente le atenderé.

A partir de aquí se puede hacer que el control se vea en la barra de herramientas de Visual Studio con un icono que nosotros le indiquemos, o también se le puede asociar una plantilla de diseño para modificar ciertas propiedades, pero esto ya es algo más avanzado y creo que la información que hay en la red es suficiente como para yo incluirla en este primer contacto.

Espero que os haya servido de ayuda.