ASP.NET Forms Authentication with Role Provider

Alternative Code for using ASP.NET Forms Authentication with the addition of a .NET 2.0 Role Provider.

You should view ASP.NET Forms Authentication before reading this this page, since the information in this page was developed from it.

One configuration option in the Web.Config file is how the pages and applications in the directory are secured. One of the ASP.NET options is to use "Forms Authentication", where access to a set of pages requires authentication through a login page.

In the Microsoft .NET Framework 2.0 you could also use the Role Provider to allow specfic access to areas of you web site. An example entry in the Web.Config is as follows:

<authentication mode="Forms">
   <forms name="casauth"
       loginUrl="SSO.aspx"
       timeout="10"
    />
</authentication>

<authorization>
     <allow roles="LDAP-Admins,LDAP-WindowsTeam" />
     <deny users="*" />
</authorization>

<roleManager defaultProvider="LDAPRoleProvider"
     enabled="true"
     cacheRolesInCookie="true"
     cookieName=".AuthenticateRoleCookie"
     cookieTimeout="10"
     cookiePath="/"
     cookieRequireSSL="false"
     cookieSlidingExpiration="true"
     cookieProtection="All" >
     <providers>
          <clear />
          <add name="LDAPRoleProvider"
               type="LDAPRoleProvider.LDAPRoleProvider"
               connectionStringName="LDAPPath"
               applicationName="LDAPRoleProvider"
          />
     </providers>
</roleManager>

This tells ASP.NET to require authentication for all access to the directory and to internally reroute every unauthenticated user to the SSO.aspx page. Additionaly the user must be also be in the role LDAP-Admins or LDAP-WindowsTeam as determined by the role provider, in this case an LDAP Role Provider. You can read the Microsoft documentation if you want a more limited scope of protection.

If you wish to create your own Role Provider Interface in Visual Studio 2005 then you actually only need to implement :

public override string[] GetRolesForUser(string username)

and

public override bool IsUserInRole(string username, string roleName)

for the role provider to work. IsUserInRole is optional and is only used in code:

Label1.Text = User.IsInRole("LDAP-WindowsTeam").ToString();

Now in Visual Studio create a login.aspx page with C# as the language. Drag a Label object to it from the Toolbox, and rename it SignonMessages and will be used for an error message if something goes wrong. There is nothing more on the "login web page". The rest is done with code.

In the generated SSO.aspx.cs file replace the code with that below and change the CASHOST to your CAS root Url.

C#

using System;
using System.Web.Security;
using System.IO;
using System.Net;
using System.Xml;
// using System.Configuration;

public partial class SSO : System.Web.UI.Page
{
    private const string CASHOST = "https://secure.its.yale.edu/cas/";

    // Or you could use the appsettings
    // private const string CASHOST = ConfigurationManager.AppSettings["CASHOST"];

    protected void Page_Load(object sender, EventArgs e)
    {
        // Look for the "ticket=" after the "?" in the URL
        string AuthorisationTicket = Request.QueryString["ticket"];

        // This page is the CAS service=, but discard any query string residue
        string UrlToAuthenticate = Request.Url.GetLeftPart(UriPartial.Path);

        // Look for a Session object called CASId
        if (Session["CASId"] != null)
        {
            // If the user gets here then either their session has timed out,
            // they have possibly logged out or the role provider has denied
            // them access to the page they have requested

            // Users will always return here if they do not have role
            // permission to view a page. The downside is, it will sign
            // them out of pages they DO have access to and they'll have to
            // start again.

            // Double check they are signed out
            string SignoutUrl = CASHOST + "logout";
            StreamReader SignoutHttpReader = new StreamReader(new WebClient().OpenRead(SignoutUrl));
            string SignOutResponse = SignoutHttpReader.ReadToEnd();

            // Sign the user out of the .NET site and Give them a message,
            // they'll never see it though...
            SignonMessages.Text = "No Permission";
            Session.Abandon();
            Session.Clear();
            FormsAuthentication.SignOut();

            // ...because they get redirected and forced to renew their
            // CAS authentication ticket.
            string RenewSignonUrl = CASHOST + "login?" + "service=" + UrlToAuthenticate + "&renew=true";
            Response.Redirect(RenewSignonUrl);
            return;
        }
        else
        {

            // First time through there is no ticket=, so redirect to CAS login
            if (AuthorisationTicket == null || AuthorisationTicket.Length == 0)
            {
                string SignonUrl = CASHOST + "login?" + "service=" + UrlToAuthenticate;
                Response.Redirect(SignonUrl);
                return;
            }

            // Second time (back from CAS) there is a ticket= to validate
            string ValidateSignonUrl = CASHOST + "serviceValidate?" + "ticket=" + AuthorisationTicket + "&" + "service=" + UrlToAuthenticate;
            StreamReader ValidateSignonHttpReader = new StreamReader(new WebClient().OpenRead(ValidateSignonUrl));

            // I like to have the text in memory for debugging rather than parsing the stream
            string ValidateSignonResponse = ValidateSignonHttpReader.ReadToEnd();

            // Some boilerplate to set up the parse.
            NameTable XmlNT = new NameTable();
            XmlNamespaceManager XmlNSManager = new XmlNamespaceManager(XmlNT);
            XmlParserContext XmlParser = new XmlParserContext(null, XmlNSManager, null, XmlSpace.None);
            XmlTextReader XmlResponseReader = new XmlTextReader(ValidateSignonResponse, XmlNodeType.Element, XmlParser);

            string SSOUsername = null;

            // A very dumb use of XML. Just scan for the "user". If it isn't there, its an error.
            while (XmlResponseReader.Read())
            {
                if (XmlResponseReader.IsStartElement())
                {
                    string XmlTag = XmlResponseReader.LocalName;
                    if (XmlTag == "user")
                    {
                        SSOUsername = XmlResponseReader.ReadString();
                    }
                }
            }
            // if you want to parse the proxy chain, just add the logic above
            XmlResponseReader.Close();

            // If there was a problem, leave the message on the screen. Otherwise, return to original page.
            if (SSOUsername == null)
            {
                SignonMessages.Text = "CAS returned to this application, but then refused to validate your identity.";
            }
            else
            {
                // Create a session for recording the fact that we are logged on
                // and for allowing us to use .NET role management.

                // Was going to store AuthorisationTicket here but not sure if that
                // is good practice or not.
                Session["CASId"] = SSOUsername;

                // set SSOUsername in ASP.NET blocks
                FormsAuthentication.RedirectFromLoginPage(SSOUsername, false);
            }

            // Note: if you use the asp:LoginStatus control, remember the LogoutPageUrl
            // should be set to the CAS logout page and you should set the LogoutAction to Redirect
        }
    }
}