...
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 | ||||||
---|---|---|---|---|---|---|
| ||||||
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; } } } } |