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

Archivo para diciembre, 2013

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

Código C# para comprobar permisos de un fichero

Si eres uno de los que está más que harto de no encontrar un código en condiciones para comprobar si un fichero tiene permisos de lectura y escritura entonces has llegado al sitio indicado.

private static bool TienePermisosUsuarioImpersonalizado(string rutaFisica)
{
bool resultado = false;

// Esta variable pretende evitar que salte la excepción de no tener permniso cuando se lanza File.Exists
bool existeRuta = false;

// Obtiene el usuario que lanza la aplicación en la máquina
WindowsIdentity identity = WindowsIdentity.GetCurrent();

try
{
// NOTA : RublenX - Se podría comprobar por FileIOPermission para comprobar File.Exists pero como da una excepción igualmente no se hace así
// Más info en : http://msdn.microsoft.com/es-es/library/vstudio/system.security.permissions.fileiopermission(v=vs.100).aspx
existeRuta = File.Exists(rutaFisica);
}
catch (SecurityException)
{
// Si no hay permisos ni siquiera para acceder al fichero entonces se da por excluido
existeRuta = false;
}

if (!string.IsNullOrEmpty(rutaFisica) && identity != null && existeRuta)
{
// Primero comprueba que el fichero no esté en modo sólo lectura
FileAttributes atributos = File.GetAttributes(rutaFisica);
if ((atributos & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
resultado = false;
}
else
{
// Ahora comprueba que el usuario logado tiene permisos de escritura y lectura
FileSecurity fSecurity = File.GetAccessControl(rutaFisica);
if (fSecurity != null)
{
AuthorizationRuleCollection rules = fSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier));

// Recorre las reglas para todos los usuarios incluidos
foreach (AuthorizationRule rule in rules)
{
FileSystemAccessRule ruleConvertida = rule as FileSystemAccessRule;

if (ruleConvertida != null)
{
// NOTA : RublenX Tal vez se podía haber hecho con Environment.UserName, pero esta forma me gusta mucho más ya que es el usuario que
// ha lanzado la aplicación
if (ruleConvertida.IdentityReference.Value == identity.Owner.Value && ruleConvertida.AccessControlType == AccessControlType.Allow && (ruleConvertida.FileSystemRights & (FileSystemRights.Read | FileSystemRights.Write)) == (FileSystemRights.Read | FileSystemRights.Write))
{
// Una de sus reglas es del usuario que ha lanzado la aplicación con permisos de lectura y escritura
resultado = true;
break;
}
}
}
}
}
}
return resultado;
}

El código es capaz de comprobar si el usuario que lanza la aplicación es capaz de llegar primero al fichero, luego comprueba que no está en modo de sólo lectura, para terminar recorriendo los permisos de ese usuario y ver si son de lectura y escritura.

También puede pasar que necesitéis suplantar el usuario original que ha lanzado la aplicación por otro que tenga permisos de lectura y escritura. En ese caso la siguiente clase os puede ser muy útil:

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace RublenX.PermisosDeAcceso
{
/// <summary>
/// Clase que permite suplantar el usuario identificado que ejecuta la aplicación en la máquina servidora
/// </summary>
public class SuplantarUsuarioIISBL : IDisposable
{
#region Librerías Externas
public const int LOGON32_LOGON_INTERACTIVE = 2;
public const int LOGON32_PROVIDER_DEFAULT = 0;

WindowsImpersonationContext impersonationContext;

[DllImport("advapi32.dll")]
public static extern int LogonUserA(String lpszUserName,
String lpszDomain,
String lpszPassword,
int dwLogonType,
int dwLogonProvider,
ref IntPtr phToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken,
int impersonationLevel,
ref IntPtr hNewToken);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool RevertToSelf();

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern bool CloseHandle(IntPtr handle);
#endregion

#region Métodos Públicos
/// <summary>
/// Inicia la suplantación de usuario por el indicado en los parámetros
/// </summary>
/// <param name="userName">Usuario</param>
/// <param name="domain">Dominio de autenticación</param>
/// <param name="password">Contraseña</param>
/// <returns>True si consigue validar al usuario e impersonalizarlo</returns>
public bool ImpersonateValidUser(String userName, String domain, String password)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(domain))
{
WindowsIdentity tempWindowsIdentity;
IntPtr token = IntPtr.Zero;
IntPtr tokenDuplicate = IntPtr.Zero;

if (RevertToSelf())
{
if (LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, ref token) != 0)
{
if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)
{
tempWindowsIdentity = new WindowsIdentity(tokenDuplicate);
impersonationContext = tempWindowsIdentity.Impersonate();
if (impersonationContext != null)
{
CloseHandle(token);
CloseHandle(tokenDuplicate);
return true;
}
}
}
}
if (token != IntPtr.Zero)
CloseHandle(token);
if (tokenDuplicate != IntPtr.Zero)
CloseHandle(tokenDuplicate);
}
return false;
}

/// <summary>
/// Devuelve la suplantación del usuario al original
/// </summary>
public void UndoImpersonation()
{
if (impersonationContext != null)
{
impersonationContext.Undo();
impersonationContext.Dispose();
impersonationContext = null;
}
}
#endregion

#region Implementación de la interfaz
/// <summary>
/// Revierte la suplantación de identidad si es que se ha producido y libera todos los recursos utilizados
/// </summary>
public void Dispose()
{
UndoImpersonation();
}
#endregion
}
}

Y por tanto la forma correcta de invocarlo uniendo los dos sería de esta forma:

/// <summary>
/// Comprueba si el fichero tiene permisos de lectura y escritura para el usuario indicado
/// </summary>
/// <param name="rutaFisica">Ruta del fichero</param>
/// <param name="identity">Usuario del que se obtendrán los permiosos (por ejemplo el actual WindowsIdentity.GetCurrent())</param>
/// <returns>Verdadero si tiene los permisos de lectura y escritura</returns>
public static bool TienePermisosLecturaEscritura(string rutaFisica)
{
// Resultado de la validación
bool resultado = false;

// Datos del usuario a autenticar
string u = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoUsuario"];
string d = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoDominio"];
string p = ConfigurationManager.AppSettings["UsuariosAppLocalAutorizados.LecturaEscrituraDiscoPassword"];

using (SuplantarUsuarioIISBL suplantarUsuarioBL = new SuplantarUsuarioIISBL())
{
if (string.IsNullOrEmpty(u))
{
resultado = TienePermisosUsuarioImpersonalizado(rutaFisica);
}
else if (suplantarUsuarioBL.ImpersonateValidUser(u, d, p))
{
resultado = TienePermisosUsuarioImpersonalizado(rutaFisica);
}
else
{
// Personalizar la excepción como os venga en gana
throw new Exception("Error en la aplicación");
}
}

// Devuelve el resultaod de la validación de permisos
return resultado;
}

Espero que os guste.

A %d blogueros les gusta esto: