CASP Adds CAS Logic to an ASP.NET App

CASP (CAS for ASP) was originally created by John Tantalo (john.tantalo@case.edu) at Case Western Reserve University to provide a means for ASP.NET applications to authenticate via Central Authentication Service by including and referring to a single class in the application project.

The original CASP source code was released under the BSD license, but it appears that web server is no longer in service.

In 2007 Ryan Davis (ryan@acceleration.net) enhanced CASP to support CAS2 and posted his updated version of CASP on his blog. Ryan's version is reproduced here:

CASP.cs
using System;
using System.IO;
using System.Net;
using System.Web;
using System.Web.UI;
using System.Xml;
using System.Xml.XPath;

namespace Acceleration.Net.ADWCodebase.ADWObjects.Utilities {
    /// <summary>
    /// Eases authenticating via Central Authentication Service.  This implements some of CAS2.
    ///
    /// Created by Ryan Davis, ryan@acceleration.net
    /// Acceleration.net
    /// </summary>
    /// <remarks>
    /// The code below is largely based on CASP (http://opensource.case.edu/trac_projects/CAS/wiki/CASP),
    /// created by John Tantalo (john.tantalo@case.edu) at Case Western Reserve University.
    ///
    /// This code is released under the BSD license
    /// ----
    /// Copyright (c) 2007, Ryan Davis
    ///
    /// All rights reserved.
    ///
    /// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
    ///
    ///     * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    ///     * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    ///     * Neither the name of the Acceleration.net nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
    ///
    /// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    /// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    /// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    /// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    /// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    /// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    /// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    /// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    /// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    /// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    /// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    /// </remarks>
    /// <example>
    /// Simple example to get the username using CAS2:
    /// <code>
    /// protected void Page_Load(object sender, EventArgs e) {
    ///   string username = CASP.Authenticate("https://login.case.edu/cas/", this.Page);
    ///   //do whatever with username
    /// }
    /// </code>
    /// Slightly more complex, using CAS1 and always renewing the authentication ticket:
    /// <code>
    /// protected void Page_Load(object sender, EventArgs e) {
    ///   string username = CASP.Authenticate("https://login.case.edu/cas/", this.Page, true, false);
    ///   //do whatever with username
    /// }</code>
    /// If you need to be doing custom things, you can get fine-grained control over the process:
    /// <code>
    /// protected void Page_Load(object sender, EventArgs e) {
    ///   CASP casp = new CASP("https://login.case.edu/cas/", this.Page, true); //re-login every time
    ///   if (casp.Login()) {
    ///     try {
    ///       string username = casp.ServerValidate(); //or casp.Validate() for CAS1
    ///       //do whatever with username
    ///     }catch (CASP.ValidateException ex) {
    ///       //try again, something was messed up
    ///       casp.Login(true);
    ///     }
    ///   }
    /// }
    /// </code>
    /// </example>
    public class CASP {
        /// <summary>
        /// The base URL for the CAS server.
        /// </summary>
        protected string baseCasUrl;

        /// <summary>
        /// The service URL for the client, used as the "service" parameter in all CAS calls.
        /// </summary>
        protected string ServiceUrl;

        /// <summary>
        /// The page we're on
        /// </summary>
        protected Page currentPage;

        /// <summary>
        /// Determines if we renew logins.  If true, CAS sessions from other browsing can be utilized.  If false, user will need to enter credentials every time.
        /// </summary>
        /// <remarks>
        /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.1.1
        /// </remarks>
        protected bool AlwaysRenew;

        /// <summary>
        /// The CAS ticket on the current page.
        /// </summary>
        public string CASTicket {
            get { return currentPage.Request.QueryString["ticket"]; }
        }

        /// <summary>
        /// Create a new CASP object, setting some initial values
        /// </summary>
        /// <param name="baseCasUrl">eg: "https://login.case.edu/cas/"</param>
        /// <param name="currentPage">usually this.Page or this</param>
        /// <param name="alwaysRenew">true to always renew CAS logins (prompting for credentials every time)</param>
        public CASP(string baseCasUrl, Page currentPage, bool alwaysRenew) {
            if (currentPage == null)
                throw new ArgumentNullException("currentPage cannot be null");
            if (baseCasUrl == null)
                throw new ArgumentNullException("baseCasUrl cannot be null");

            this.baseCasUrl = baseCasUrl;
            this.currentPage = currentPage;
            this.AlwaysRenew = alwaysRenew;
            ServiceUrl = HttpUtility.UrlEncode(currentPage.Request.Url.AbsoluteUri.Split('?')[0]);
        }

        /// <summary>
        /// Create a new CASP object, setting some initial values
        /// </summary>
        /// <param name="baseCasUrl">eg: "https://login.case.edu/cas/"</param>
        /// <param name="currentPage">usually this.Page or this</param>
        public CASP(string baseCasUrl, Page currentPage)
            : this(baseCasUrl, currentPage, false) {}

        /// <summary>
        /// Validates using CAS2, returning the value of the node given in the xpath expression.
        /// </summary>
        /// <remarks>
        /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.5
        /// </remarks>
        /// <param name="xpath"></param>
        /// <exception cref="ValidateException">If an error occurs</exception>
        /// <returns></returns>
        public string ServiceValidate(string xpath) {
            string result;
            //get the CAS2 xml into a string
            string xml =
                GetResponse(
                    string.Format("{0}?ticket={1}&service={2}", Path.Combine(baseCasUrl, "serviceValidate"), CASTicket,
                                  ServiceUrl));
            try {
                //use an army of objects to run an xpath on that xml string
                using (TextReader tx = new StringReader(xml)) {
                    XPathNavigator nav = new XPathDocument(tx).CreateNavigator();
                    XPathExpression xpe = nav.Compile(xpath);

                    //recognize xmlns:cas
                    XmlNamespaceManager namespaceManager = new XmlNamespaceManager(new NameTable());
                    namespaceManager.AddNamespace("cas", "http://www.yale.edu/tp/cas");
                    xpe.SetContext(namespaceManager);

                    //get the contents of the <cas:user> element
                    XPathNavigator node = nav.SelectSingleNode(xpe);
                    result = node.Value;
                }
            }
            catch (Exception ex) {
                //if we had a problem somewhere above, throw up with some helpful data
                throw new ValidateException(CASTicket, xml, ex);
            }

            return result;
        }

        /// <summary>
        /// Validates using CAS2, returning the cas:user
        /// </summary>
        /// <returns>returns the value of the cas:user</returns>
        public string ServiceValidate() {
            return ServiceValidate("/cas:serviceResponse/cas:authenticationSuccess/cas:user");
        }

        /// <summary>
        /// Validates a ticket using CAS1, returing the username
        /// </summary>
        /// <remarks>
        /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.4
        /// </remarks>
        /// <exception cref="ValidateException">If an error occurs</exception>
        /// <returns></returns>
        public string Validate() {
            string result;
            //get the CAS1 response into a string
            string resp =
                GetResponse(
                    string.Format("{0}?ticket={1}&service={2}", Path.Combine(baseCasUrl, "validate"), CASTicket,
                                  ServiceUrl));
            try {
                result = resp.Split('\n')[1];
            }
            catch (Exception ex) {
                //if we had a problem somewhere above, throw up with some helpful data
                throw new ValidateException(CASTicket, resp, ex);
            }
            return result;
        }

        /// <summary>
        /// Logs in the user, redirecting if needed.
        /// </summary>
        /// <remarks>
        /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.1
        /// </remarks>
        /// <param name="serviceUrl"></param>
        /// <param name="force">force the redirect, whether we have a ticket or not</param>
        /// <returns>Returns true if we're logged in and ready to be validated.</returns>
        public bool Login(string serviceUrl, bool force) {
            string loginUrl = string.Format("{0}?service={1}",
                                            Path.Combine(this.baseCasUrl, "login"), serviceUrl);
            if (AlwaysRenew)
                loginUrl += "&renew=true";

            if (force || string.IsNullOrEmpty(CASTicket))
                currentPage.Response.Redirect(loginUrl, true);

            return !(force || string.IsNullOrEmpty(CASTicket));
        }

        /// <summary>
        /// Logs in the user, redirecting if needed.
        /// </summary>
        /// <param name="force">force the redirect, whether we have a ticket or not</param>
        /// <returns>Returns true if we're logged in and ready to be validated.</returns>
        public bool Login(bool force) {
            return Login(ServiceUrl, force);
        }

        /// <summary>
        /// Logs in the user, redirecting if needed.
        /// </summary>
        /// <returns>Returns true if we're logged in and ready to be validated.</returns>
        public bool Login() {
            return Login(ServiceUrl, false);
        }

        /// <summary>
        /// Helper to get a web response as text
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        protected static string GetResponse(string url) {
            //split out IDisposables into seperate using blocks to ensure everything gets disposed
            using (WebClient c = new WebClient())
            using (Stream response = c.OpenRead(url))
            using (StreamReader reader = new StreamReader(response)) {
                return reader.ReadToEnd();
            }
        }

        /// <summary>
        /// Authenticates, getting the username.  Will redirect as needed.
        /// </summary>
        /// <param name="baseCasUrl">eg: "https://login.case.edu/cas/"</param>
        /// <param name="page">usually this.Page or this</param>
        /// <param name="alwaysRenew">true to always renew CAS logins (prompting for credentials every time)</param>
        /// <param name="useCas2">if set to <c>true</c> then use CAS2 ServiceValidate, otherwises uses CAS1 Validate</param>
        /// <returns>username</returns>
        public static string Authenticate(string baseCasUrl, Page page, bool alwaysRenew, bool useCas2) {
            string username = null;
            CASP casp = new CASP(baseCasUrl, page, alwaysRenew);
            if (casp.Login()) {
                try {
                    username = useCas2 ? casp.ServiceValidate() : casp.Validate();
                }
                catch (ValidateException) {
                    //try again, something was messed up
                    casp.Login(true);
                }
            }
            return username;
        }
        /// <summary>
        /// Authenticates using CAS2, getting the username.  Will redirect as needed.
        /// </summary>
        /// <param name="baseCasUrl"></param>
        /// <param name="page"></param>
        /// <returns>cas:user</returns>
        public static string Authenticate(string baseCasUrl, Page page) {
            return Authenticate(baseCasUrl, page, false, true);
        }

        /// <summary>
        /// Represents errors when validating a CAS ticket
        /// </summary>
        public class ValidateException : Exception {
            /// <summary>
            /// The actual response from the server
            /// </summary>
            public string ValidationResponse;

            /// <summary>
            /// Throws a new one, crafting a decent exception message.
            /// </summary>
            /// <param name="ticket">The CAS ticket.</param>
            /// <param name="validationResponse">The validation response.</param>
            /// <param name="innerException">The inner exception.</param>
            public ValidateException(string ticket, string validationResponse, Exception innerException)
                : base(
                    string.Format("Error validating ticket {0}, validation response:\n{1}", ticket, validationResponse),
                    innerException) {
                this.ValidationResponse = validationResponse;
            }
        }
    }
}