Páginas

SSO ASP.NET to WordPress: ASP.NET Souce Code

Este post es la tercera parte del post SSO ASP.NET to WordPress, en donde puedes encontrar la explicación general del proyecto. Aquí puedes encontrar el código fuente ASP.NET utilizado para establecer un 'Single Sign On' (SSO) entre una aplicación web ASP.NET (AppW) y un WordPress (WP).

Tal y como se indica en el post principal, los dos archivos necesarios para el SSO son un PHP cuya explicación se encuentra en el post SSO ASP.NET to WordPress: PHP Souce Code y un archivo .cs (al que llamaré LoginWordPress.cs - el nombre puede ser modificado al gusto de cada uno -) con las clases del proyecto .NET.

ARCHIVO DE CLASES LoginWordPress.cs

El archivo de clases está divido en tres partes bien diferenciadas.

Primero tenemos la estructura básica, que comprende las tres clases juntas y un modelo (aunque ya sabeís que se deberían implementar por separado, según los principios SOLID). Aquí están englobadas todas las clases utilizadas:

1 - Un modelo para la gestión del usuario: LoginUser
2 - Una clase para controlar las Excepciones: LoginWPException
3 - Una clase con metodos auxiliares que controlan la información recibida y la gestión de las cookies
4 - La clase principal con los métodos de acceso al WordPress.

using [...];
namespace Domain.Login.Core
{
    public class LoginUser {}
    public class LoginWPException : Exception {}
    public class LoginWPHelpers {}
    public class LoginWPMethods {}
}

Procedamos a implementar cada una de las clases.

LoginUser
Esta clase, implementa el modelo para gestionar toda la información de los usuarios registrados, y puede modificarse según las necesidades de cada uno. Sirva simplemente para controlar toda la información en un único punto, y facilitar así su uso para la autenticación directa en nuestra aplicación web AppW.

Contiene una serie de atributos para almacenar los datos, unos constructores genéricos y un operador override ToString(), que yo utilizo para almacenar la información del usuario autenticado en HttpContext.Current.User.Identity.Ticket.UserData

Este sería el código fuente final:

   [Serializable]
   public class LoginUser
   {
        public int ID { get; set; }
        public string Login { get; set; }
        public string Pass { get; set; }
        public string NiceName{ get; set; }
        public string Email { get; set; }

        public LoginUser() : this("", "", -1, "", "") { }
        public LoginUser(string _user, string _pass) : this(_user, _pass, -1, "", "") { }
        public LoginUser(string _user, string _pass, int _id, string _nicename, string _email)
        {
            ID = _id;
            Login = _pass;
            Pass = _user;
            NiceName = _nicename;
            Email = _email;
        }


        /// <summary>
        /// Operator to convert ClassObject - ToString
        /// </summary>
        /// <returns>#ID|#Login|#Pass|#NiceName|#Email data string</returns>
        public override string ToString()
        {
            return ID + "|" + Login + "|" + Pass + "|" + NiceName + "|" + Email;
        }
    }

LoginWPException:
Esta es una clase de gestión de excepciones que he generado, para controlar aquellos errores que sean derivados de la propia conexión con el WP. De esta forma, el código quedaría:

   public class LoginWPException : Exception
   {
        public LoginWPException(Exception e) : this(e.Message.ToString(), e) { }
        public LoginWPException(string message) : this(message, new Exception()) { }
        public LoginWPException(string message, Exception e) : base(message, e) { }
   }

LoginWPHelpers:
Esta clase contiene métodos auxiliares que se utilizan en la clase base o principal, y que he separado para un mejor control del código fuente. Dentro de esta clase, encontramos tres métodos:

1- ParseXmlData: Esta funcion tiene un parametro XmlDocument, que es convertido al modelo LoginUser. Se utiliza para transformar la información recibida desde la página 'wp-login-asp.php' del WP. Si detecta que existe un error (por la información que existe dentro del xml) lanza una WPException.

2- GetCookieContainer: Esta función, se encarga de almacenar las cookies actuales de la aplicación en un contenedor, convirtiendo las Web.HttpCookies a Net.Cookies, que se pueden utilizar en las llamadas WebHttpRequest.

NOTA: Aquí es dónde entra en juego la verificación de los usuarios registrados en el WP. Dado que tenemos la información compartida en cookies, si intentamos realizar una petición directa vía HttpWebRequest, el WP no será capaz de devolvernos ninguna información puesto que cada llamada es, por así decirlo, 'anónima'. Necesitamos enviar las cookies en un contenedor, y así, al recibirlas el archivo php, será capaz de gestionar la información y devolvernos al usuario registrado. (Esto me llevo bastante tiempo averiguarlo, y gracias al envío de las cookies, ambos sistemas pueden funcionar como uno sólo)

3- TransferCookieToApplication: De forma análoga a la anterior función, ésta se encarga de transferir las cookies recibidas desde la HttpWebRequest, a HttpCookies comprensibles por nuestra AppW.

El código fuente final de esta clase quedaría así:

 public class LoginWPHelpers
 {
  protected internal static LoginUser ParseXmlData(XmlDocument xmlDoc)
  {
   //el usuario es correcto, actualizamos la informacion
   LoginUser current = null;
   int valid = int.Parse(xmlDoc.SelectSingleNode("//object/is_valid").InnerText);
   //read XML and parse data
   if (valid < 1)
   {
    throw new LoginWPException(xmlDoc.SelectSingleNode("//object/error_message").InnerXml);
   }
   else
   {
    current = new LoginUser();
    current.ID = int.Parse(xmlDoc.SelectSingleNode("//object/id").InnerText);
    current.Login = xmlDoc.SelectSingleNode("//object/user_login").InnerText;
    current.Pass = xmlDoc.SelectSingleNode("//object/user_pass").InnerText;
    current.NiceName = xmlDoc.SelectSingleNode("//object/user_displayname").InnerText;
    current.Email = xmlDoc.SelectSingleNode("//object/user_email").InnerText;
   }
   return current;
  }


  protected internal static CookieContainer GetCookieContainer
    (HttpCookieCollection collection, string host)
  {
   CookieContainer cc = new CookieContainer();
   foreach (string cookie_name in collection)
   {
    HttpCookie cookie = collection[cookie_name];
    Cookie cCookie = new Cookie(cookie.Name, cookie.Value, cookie.Path);
    if (!String.IsNullOrEmpty(cookie.Domain))
     cCookie.Domain = cookie.Domain;
    else
     cCookie.Domain = "." + host;
    cCookie.Expires = cookie.Expires;
    cc.Add(cCookie);
   }
   return cc;
  }


  protected internal static void TransferCookieToApplication(Cookie c, string host)
  {
   //Write the wordpress cookie to the browser
   HttpCookie cCookie = new HttpCookie(c.Name);
   cCookie.Value = c.Value;
   cCookie.Expires = c.Expires;
   cCookie.Domain = "." + host;
   cCookie.Path = "/";


   HttpContext.Current.Response.Cookies.Add(cCookie);
  }
 }

LoginWPMethods:
Y aquí tenemos el 'alma mater' de nuestra aplicación... el corazón que hace que todo esto funcione. Veamos los métodos que están implementados en ella:

1- LoginToWordpress, LogoutToWordpress, LoggedStatusWordpress: Estas funciones son las encargadas de realizar la petición a la página .php y devolver la información del usuario registrado. En caso de no tener un usuario registrado, el propio sistema lanza una LoginWPException que debe ser correctamente gestionada. Cualquier otro error, se gestiona con una Exception normal. Cada una de las funciones envía el tipo de acción correspondiente, y los datos necesarios para ejecutar la acción.

Así, las tres funciones envían la url de acceso al php, y sólo LoginToWordpress envía los datos de usuario.

2- CallHttpWebRequest: Es la funcion privada que unifica las tres funciones públicas y se encarga de hacer la petición, obteniendo el Xml resultante.

3-GetXmlRequestData: Es la función que realiza el HttpWebRequest.

NOTA: Ésta última función tiene la particularidad (y diferencia con otros post que me sirvieron de guía) de que en vez de utilizar un CookieContainer vacio para enviar/recibir las cookies, genera uno propio con las cookies de nuestra aplicación. Es precisamente esta opción la que nos permite trabajar con ambas aplicaciones.

El código fuente final de esta clase quedaría así: 

 public class LoginWPMethods
  {
    public static LoginUser LoginToWordpress(string user, string pass, Uri url_loggin) {
      return CallHttpWebRequest("&Action=loggin&UserName=" + user + "&Pwd=" + pass, url_loggin);
    }
    public static void LogoutToWordpress(Uri url_loggin) {
      CallHttpWebRequest("&Action=logout", url_loggin);
    }
    public static LoginUser LoggedStatusWordpress(Uri url_loggin) {
      return CallHttpWebRequest("&Action=logged_status", url_loggin);
    }

    private static LoginUser CallHttpWebRequest(string data, Uri url_loggin)
    {
      LoginUser current = null;
      //TRY to get and parse data HttpWebRequest
      try
      {
        XmlDocument xmlDoc = GetXmlRequestData(data, url_loggin);
      current = LoginWPHelpers.ParseXmlData(xmlDoc);
      }
      catch (LoginWPException WPExc) { /* TODO - Error logic on WP access */ }
      catch (Exception exception) { throw exception; }
      finally {}
      return current;
    }

    private static XmlDocument GetXmlRequestData(string post_data, Uri url_loggin)
    {
      ASCIIEncoding encoding = new ASCIIEncoding();
      byte[] data = encoding.GetBytes(post_data);

      //Prepare web request...
      HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(url_loggin);
      myRequest.Method = WebRequestMethods.Http.Get;
      myRequest.Method = "POST";
      myRequest.ContentType = "application/x-www-form-urlencoded";
      myRequest.ContentLength = data.Length;
      myRequest.CookieContainer = LoginWPHelpers.GetCookieContainer(
         HttpContext.Current.Request.Cookies, url_loggin.Host);
      Stream newStream = myRequest.GetRequestStream();

      //submit the php form for BuddyPress signup
      newStream.Write(data, 0, data.Length);
      newStream.Close();

      //Get the response, and pass it to a XML
      HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();
      StreamReader reader = new StreamReader(myResponse.GetResponseStream());
      string string_reader = reader.ReadToEnd();

      //Look for cookies in the response
      if (myResponse.Cookies.Count > 0)
      foreach (Cookie c in myResponse.Cookies)
        LoginWPHelpers.TransferCookieToApplication(c, url_loggin.Host);

      //close response
      myResponse.Close();

      //parse XML Data
      XmlDocument xmlDoc = new XmlDocument();
      xmlDoc.LoadXml(string_reader);
      return xmlDoc;
    }
  }


Y con esto, tendríamos toda la lógica de gestión de usuarios para obtener un 'Single Sign On entre un WordPress y una Aplicación Web ASP.NET'.

FUNCIONES DE CONEXIÓN CON WP

Por último, y no menos importante, las funciones para el uso de todas estas funciones...

Una función para realizar el Login desde la aplicación Web:

 
    protected void Login_Click(object sender, EventArgs e)
    {
      try
      {
      //set loggin, create ticket if exist user AND STORE IN SESSION
      SessionHelper.CurrentUser = LoginWPMethods.LoginToWordpress(user_login.Text, user_pass.Text, new Uri(ConfigurationManager.AppSettings["PathWP"]));
      //esta funcion crea un ticket de FormsAuthentication para el acceso de usuarios
      LoginMethods.LogIn(SessionHelper.CurrentUser);
      Response.Redirect("~");
      }
      catch (Exception ee)
      {
      ScriptManager.RegisterClientScriptBlock(this, this.GetType(), "alertLoggin", "MostrarAlerta('" + HttpUtility.JavaScriptStringEncode(ee.Message.ToString()) + "')", true);
      }
      finally { }
    }

Otra para desconectar o LogOut:
  public static void LogOut()
  {
      //eliminamos el ticket actual, y desconectamos del wordpress
      EliminarTicket();
      LoginWPMethods.LogoutToWordpress(new Uri(ConfigurationManager.AppSettings["PathWP"]));

      //borramos todo
      HttpContext.Current.Session.Clear();
      HttpContext.Current.Session.Abandon();
      HttpContext.Current.Request.Cookies.Clear();

      //redireccionamos al loginpage
      FormsAuthentication.RedirectToLoginPage();
  }

Y una función, LogginStatus, y que debe controlar las páginas que estén bajo registro (yo inserto esta función en una MasterPage, para controlar los accesos), que averiguará si existe un usuario registrado en WP o no.

Para evitar peticiones constantes al WP, dicha función sólo hace las llamadas si detecta que existen cookies de wp_loggin_id y el usuario actual no está autenticado en la AppW. De esta forma, sólo si hay un usuario WP y no AppW, se efectúa la llamada al php y obtenemos los datos del usuario para autenticarlo.

 
        public static LoginUser LogginStatus()
        {
            //set current user
            LoginUser current = null;
            //check if user is logged
            bool authenticated = HttpContext.Current.User.Identity.IsAuthenticated;
            //check if WP logged
            HttpCookie WP_Cookie = null;
            foreach (string c in HttpContext.Current.Request.Cookies)
                if (c.IndexOf("wordpress_logged_in_") != -1) WP_Cookie = HttpContext.Current.Request.Cookies[c];
            //VERIFICACIONES
            if (authenticated)
            {
                if (WP_Cookie == null && (HttpContext.Current.Request.Url.Host!="localhost")) LogOut(); //el usuario NO ESTÁ autenticado en WP, desconectamos
                else { current = GetUsuarioAutenticado(); }
            }
            else
            {
                if (WP_Cookie != null)
                {
                    //el usuario ESTÁ autenticado en WordPress. TOMAMOS LOS DATOS
                    current = LoginWPMethods.LoggedStatusWordpress(new Uri(ConfigurationManager.AppSettings["PathWP"]));
                    LogIn(current);
                }
            }
            return current;
        }


Y con esto ya hemos conseguido una integración total...
Os incluyo los enlaces a los otros post que hacen referencia al mismo tema:
· SSO ASP.NET to WordPress: Single Sign On
· SSO ASP.NET to WordPress: PHP Souce Code
Si os ha gustado y sobre todo, si os ha servido, no dudeis en valorarlo! +1
Un saludo a todos!

5 comentarios:

  1. Respuestas
    1. Sorry, but I can not publish the source code files. All lines of code in this post, are global implementations that anyone can use. The purpose of this post is to be a guide to development, for anyone to perform SSO implementations.

      If done step by step each and every one of the three examples, you will get the SSO smoothly. The first post is a general explanation, while the other two, spelling out how to program in ASP.NET and PHP environment respectively.

      Regards

      Eliminar
  2. Hola Sr. Serrano
    He seguido su solución tan de cerca.
    Es muy interesante. Actualmente estoy tratando de
    para implementar su solución, pero lamentablemente
    dos núcleos y métodos importantes faltan decisiones
    la solución incompleta.

    a - GetCookieContainer
    b - TransferCookieToApplication

    Sírvase volver a publicar esta solución o mejor aún
    es posible que Bandeja de entrada (correo electrónico) me (jcc_nnannah@yahoo.co.uk)
    con los archivos originales
    Cuando pegarlos directamente en su blog que
    gabbles cosas.

    Usted ayudará mucho porque tengo
    Ya pasó un tiempo considerable con este
    Muchas gracias

    ResponderEliminar
    Respuestas
    1. Hi James,

      I do not understand your mesaje, or its purpose. As I interpret, you're asking about the development of the two methods you write.

      Such methods are under the heading 'LoginWPHelper', with all the source code needed for its operation.

      If your question is another, you can ask about it in your native language.

      Regards.

      Eliminar
    2. Hello Rafael
      sorry pls. I used the google translator. It obviously did a bad job.

      I am very much interested in your solution and I followed it closely. But I could not find the declaration of two methods which was used later in the solution.

      The important missing methods are
      a - GetCookieContainer
      b - TransferCookieToApplication

      I do not know if it is the google translator that is messing up the files. When u copy from the blog and paste on the IDE it requires a lot of cleaning up with may introduce bugs

      Please repost this solution or better yet you may inbox (email) me (jcc_nnannah @ yahoo.co.uk) with the original files.

      It is quite interesting...

      Thank you for your patience with me

      James

      Eliminar