Custom authentication

Článkov na túto tému je veľa, prístup, ktorý používam je k dispozícii už do .NET Framework 1.1. Ponúkam skôr riešenie, ako urobiť kód znovupoužiteľný, ako si zjednodušiť prácu pri nových projektoch. Infraštruktúra providerov, ktoré ponúka Microsoft v rámci ASP .NET frameworku je ťažkopádna, využíva množstvo zložitých tabuliek a procedúr, z čoho využijeme naozaj len časť funkcionality. Veľmi dobrou alternatívou je použiť Altairis Simple ASP.NET Providers od Michala Valáška. Lenže čo keď používame ako DB PostGresSQL napr.? Po niekoľkých dňoch svedomitej implementácie AuthenticationProvidera a uvedomení si, čo mám pred sebou v podobe RoleProvidera a ProfileProvidera som snaženie vzdal aj vzhľadom na známe obmzenia/problémy napr. profile providera a vrátil sa ku starému overenému spôsobu.
  • Základné údaje o používateľovi sú uložené zakryptované do cookie, netreba ich pri každom requeste ťahať z databázy, ani ukladať do session, ako sa to bežne rieši
  • Kód autentifikácie je na rozdiel od MembershipProvidera plne pod kontrolou, jednoduchý, customizovateľný a efektívny
  • Rozšírenie a customizácia základných atribútov identity je veľmi jednoduchá

Základná infraštruktúra

Pozostáva z CustomIdentity,CustomPrincipalu a CustomAuthentication HttpModulu. Ak si vystačíme so základnými atribútmi

public class CustomIdentity : IIdentity

    {

        private int _id;

        private string _login, _displayName;

        private const string AUTHENTICATION_TYPE = "CustomAuthentication";

 

        public CustomIdentity() { }

        public CustomIdentity(string login)

        {

            _login = login;

        }

 

        public int ID

        {

            get { return _id; }

            set { _id = value; }

        }

 

        public string Login

        {

            get { return _login; }

            set { _login = value; }

        }

 

        public string DisplayName

        {

            get { return _displayName; }

            set { _displayName = value; }

        }

 

        public virtual object SaveState()

        {

            Triplet oState = new Triplet(_id, _login, _displayName);

            return oState;

        }

 

        public virtual void LoadState(object state)

        {

            Triplet props = state as Triplet;

            if (props != null)

            {

                ID = (int)props.First;

                Login = (string)props.Second;

                DisplayName = (string)props.Third;

            }

        }

 

        public string AuthenticationType

        {

            get { return AUTHENTICATION_TYPE; }

        }

 

        public bool IsAuthenticated

        {

            get { return !string.IsNullOrEmpty(_login); }

        }

 

        public string Name

        {

            get { return _login; }

        }       

    }

public class CustomPrincipal : IPrincipal

    {

        private string[] _roles;

        private CustomIdentity _identity;

 

        public string[] Roles

        {

            get { return _roles; }

            set { _roles = value; }

        }

 

        public virtual IIdentity Identity

        {

            get { return _identity; }

        }

 

        /// <summary>

        /// Required due to deserialization

        /// </summary>

        public CustomPrincipal()

        {

            _identity = CreateIdentity();           

        }

 

        public CustomPrincipal(CustomIdentity identity, string[] roles)

        {

            _identity = identity;

            _roles = roles;

        }

 

        protected virtual CustomIdentity CreateIdentity()

        {

            return new CustomIdentity();

        }

 

        public virtual object SaveState()

        {

            return new Pair(((CustomIdentity)Identity).SaveState(), _roles);

        }

 

        public virtual void LoadState(object state)

        {

            Pair props = state as Pair;

            if (props != null)

            {

                if (_identity == null)

                    _identity = new CustomIdentity();

                ((CustomIdentity)Identity).LoadState(props.First);

                _roles = (string[])props.Second;

            }

        }

 

        public virtual void LoadStateFromBase64String(string sState)

        {

            byte[] arrBytes = Convert.FromBase64String(sState);

            object state = null;

 

            if (arrBytes.Length != 0)

            {

                BinaryFormatter binFormatter = new BinaryFormatter();

 

                using (MemoryStream memStrm = new MemoryStream(arrBytes))

                    state = binFormatter.Deserialize(memStrm);

                LoadState(state);

            }

        }

 

        public virtual string SaveStateAsBase64String()

        {

            object state = SaveState();

 

            byte[] arrBytes = new byte[0] { };

 

            if (state != null)

            {

                BinaryFormatter binFormatter = new BinaryFormatter();

                using (MemoryStream memStrm = new MemoryStream())

                {

                    binFormatter.Serialize(memStrm, state);

                    arrBytes = memStrm.ToArray();

                }

            }

            return Convert.ToBase64String(arrBytes);

        }

 

        IIdentity IPrincipal.Identity

        {

            get { return _identity; }

        }

 

        public bool IsInRole(string role)

        {

            if (_roles == null)

                return false;

            return ((IList)_roles).Contains(role);

        }

 

        public bool IsInAnyRole(IList roles)

        {

            foreach (string role in roles)

                if (((IList)_roles).Contains(role))

                    return true;

            return false;

        }       

    }

public class CustomAuthenticationModule : IHttpModule

    {

        public void Init(HttpApplication application)

        {

            application.AuthenticateRequest += (new EventHandler(this.Application_AuthenticateRequest));

        }

 

        private void Application_AuthenticateRequest(Object oSource, EventArgs oEArgs)

        {

            HttpApplication application = (HttpApplication)oSource;

            HttpContext context = application.Context;

 

            if (!DoAuthenticateRequest(context))

                return;

 

            HttpCookie authenCookie = context.Request.Cookies[FormsAuthentication.FormsCookieName];

            if (authenCookie == null)

                return;

 

            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authenCookie.Value);

 

            if (ticket.Expired)

                return;

 

            CustomPrincipal principal = CreatePrincipal();

 

            principal.LoadStateFromBase64String(ticket.UserData);

            context.User = principal;

        }

 

        protected virtual CustomPrincipal CreatePrincipal()

        {

            return new CustomPrincipal();

        }

 

        /// <summary>

        /// This method decides based upon the supplied HtppContext whether the access to the requested resource needs to be authenticated or not.

        /// By default, all requests are authenticated, override this method in a child class to change this behavior.

        /// </summary>

        /// <param name="context">The HttpContext of the incoming HttpRequest</param>

        /// <returns></returns>

        protected virtual bool DoAuthenticateRequest(HttpContext context)

        {

            return true;

        }

 

        public void Dispose()

        {

        }

    }

Ak si vystačíme so základnými properties identity, možeme použiť v našej web aplikácii priamo tieto triedy. Ak potrebujem pridať ďalšie atribúty ku identite, musíme podediť všetky tri triedy. Do indentity musíme pridať atribúty v principale a autentifikačnom module preťažiť metódy, ktoré vytvárajú inštanciu identity a principala(kôly automatickej deserializácii z cookie). Pár riadkov kódu. Ja som pridal atribút company, ktorý v sebe zapuzdruje ID a názov spoločnosti, v ktorej je používateľ.

public class TestAuthenticationModule : CustomAuthenticationModule

    {   

        protected override CustomPrincipal CreatePrincipal()

        {

            return new TestPrincipal();

        }   

    }

public class TestIdentity : CustomIdentity

    {

        private Company _company;

 

        public Company Company

        {

            get { return _company; }

            set { _company = value; }

        }

 

        public TestIdentity()

        {

            _company= new Company();

        }

 

        public override object SaveState()

        {

            object state = base.SaveState();

            if (state != null)

                return new object[] { state, _company.ID, _company.Name };           

            else

                return state;           

        }

 

        public override void LoadState(object state)

        {

            object[] currentState = state as object[];

            if (currentState != null)

            {

                base.LoadState(currentState[0]);

                _company.ID = (int)currentState[1];

                _company.Name = currentState[2] as string;

            }

            else           

                base.LoadState(state);           

        }

    }

public class TestPrincipal : CustomPrincipal

    {

        protected override CustomIdentity CreateIdentity()

        {

            return new TestIdentity();

        }

    }

Kód prihlasovacej stránky je jednoduchý:
<%@ Page
    Language="C#"
    AutoEventWireup="true"
    EnableViewState="false"
    EnableSessionState="false"
    CodeBehind="Default.aspx.cs"
    Inherits="CustomIdentityApp._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Login
            ID="LoginCtrl"
            DestinationPageUrl="~/Private.aspx"
            FailureText="Invalid user or password"
            OnAuthenticate="LoginCtrl_Authenticate"
            runat="server" />        
    </div>
    </form>
</body>
</html>

using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

 

using System.Security.Principal;

using System.Security;

using System.Web.Security;

 

using ASPNET.Security.Principal;

using ASPNET.Web;

 

namespace CustomIdentityApp

{

    public partial class _Default : System.Web.UI.Page

    {       

        public void LoginCtrl_Authenticate(object sender, AuthenticateEventArgs e)

        {

            if(LoginCtrl.UserName.ToLower() != "billgates" || LoginCtrl.Password != "msdos")

            {

                e.Authenticated = false;

                return;

            }

 

            TestIdentity identity = new TestIdentity();

            identity.Login = "billGates";

            identity.ID = 1000;

            identity.DisplayName = "Bill :-) Gates";

            identity.Company.ID = 1000;

            identity.Company.Name = "Microsoft";

 

            CustomPrincipal principal = new CustomPrincipal(identity, new string[] {"Director", "Pensioner"});

 

            HttpContext.Current.User = principal;

 

            string state = principal.SaveStateAsBase64String();

 

            DateTime now= DateTime.Now;

            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,

                identity.Login,

                now,

                now.AddMinutes(60),

                LoginCtrl.RememberMeSet,

                state);

 

            string encodedTicket = FormsAuthentication.Encrypt(ticket);

 

            HttpCookie ticketCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encodedTicket);

            Response.Cookies.Add(ticketCookie);

 

            string returnUrl = Request.QueryString["ReturnUrl"];

            Response.Redirect(

                !string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/Private.aspx",

                true);

        }

    }

}


Dorobiť nejakú password policy, ako obsahuje AuthenticationProvider nie je problém, stored procedúra na overenie prihlásenia a vrátenie autenrifikačných údajov je veľmi jednoduchá. Ako (jednoducho) pracovať s hashovaným passwordom, ako security improvement, si ukážeme nabudúce.Kód sample aplikácie a infraštruktúrnej library je stiahnuteľný tu.

Nepriame pokračovanie článku: Ochrana hesla hashom a custom autentifikácia .

Komentáre

# skippo said:

Diky za clanok, triedy Triple a Pair som nepoznal, dobre vediet, ze existuju take napomocne zalezitosti.

Monday, June 25, 2007 1:07 PM
# T said:

MS ich interne dost pouziva v komponentoch pri praci so stateom, tiez som ich objavil cez reflektor :-))

Monday, June 25, 2007 3:49 PM
# Tomáš - DevBlog said:

V príspevku s veľavravným názvom :-) Custom authentication sme si ukázali, ako jednoducho sa dá nahradiť Provider funkcionalita pri forms autentifikácii niečím efektívnejším a prispôsobiteľnejším. Membership provider však ponúka oveľa viac funkcionality

Friday, June 29, 2007 2:51 PM
# kardo said:

funguje ten blog vobec :-)

Thursday, July 19, 2007 1:27 PM
# T said:

Ako to myslis? Mam teraz inu pracu, takze nemam cas prispievat a tento blog system ma IMHO dost much, alebo ich mam ja v jeho pouzivani :-)

Friday, July 20, 2007 8:14 AM
Prihlásiť | Registrovať | Pomoc