Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

...

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:

Code Block
borderStylesolid
titleCASP.csborderStylesolid
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;
            }
        }
    }
}