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 { /// /// Eases authenticating via Central Authentication Service. This implements some of CAS2. /// /// Created by Ryan Davis, ryan@acceleration.net /// Acceleration.net /// /// /// 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. /// /// /// Simple example to get the username using CAS2: /// /// protected void Page_Load(object sender, EventArgs e) { /// string username = CASP.Authenticate("https://login.case.edu/cas/", this.Page); /// //do whatever with username /// } /// /// Slightly more complex, using CAS1 and always renewing the authentication ticket: /// /// 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 /// } /// If you need to be doing custom things, you can get fine-grained control over the process: /// /// 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); /// } /// } /// } /// /// public class CASP { /// /// The base URL for the CAS server. /// protected string baseCasUrl; /// /// The service URL for the client, used as the "service" parameter in all CAS calls. /// protected string ServiceUrl; /// /// The page we're on /// protected Page currentPage; /// /// 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. /// /// /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.1.1 /// protected bool AlwaysRenew; /// /// The CAS ticket on the current page. /// public string CASTicket { get { return currentPage.Request.QueryString["ticket"]; } } /// /// Create a new CASP object, setting some initial values /// /// eg: "https://login.case.edu/cas/" /// usually this.Page or this /// true to always renew CAS logins (prompting for credentials every time) 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]); } /// /// Create a new CASP object, setting some initial values /// /// eg: "https://login.case.edu/cas/" /// usually this.Page or this public CASP(string baseCasUrl, Page currentPage) : this(baseCasUrl, currentPage, false) {} /// /// Validates using CAS2, returning the value of the node given in the xpath expression. /// /// /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.5 /// /// /// If an error occurs /// 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 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; } /// /// Validates using CAS2, returning the cas:user /// /// returns the value of the cas:user public string ServiceValidate() { return ServiceValidate("/cas:serviceResponse/cas:authenticationSuccess/cas:user"); } /// /// Validates a ticket using CAS1, returing the username /// /// /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.4 /// /// If an error occurs /// 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; } /// /// Logs in the user, redirecting if needed. /// /// /// See http://www.ja-sig.org/products/cas/overview/protocol/index.html, section 2.1 /// /// /// force the redirect, whether we have a ticket or not /// Returns true if we're logged in and ready to be validated. 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)); } /// /// Logs in the user, redirecting if needed. /// /// force the redirect, whether we have a ticket or not /// Returns true if we're logged in and ready to be validated. public bool Login(bool force) { return Login(ServiceUrl, force); } /// /// Logs in the user, redirecting if needed. /// /// Returns true if we're logged in and ready to be validated. public bool Login() { return Login(ServiceUrl, false); } /// /// Helper to get a web response as text /// /// /// 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(); } } /// /// Authenticates, getting the username. Will redirect as needed. /// /// eg: "https://login.case.edu/cas/" /// usually this.Page or this /// true to always renew CAS logins (prompting for credentials every time) /// if set to true then use CAS2 ServiceValidate, otherwises uses CAS1 Validate /// username 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; } /// /// Authenticates using CAS2, getting the username. Will redirect as needed. /// /// /// /// cas:user public static string Authenticate(string baseCasUrl, Page page) { return Authenticate(baseCasUrl, page, false, true); } /// /// Represents errors when validating a CAS ticket /// public class ValidateException : Exception { /// /// The actual response from the server /// public string ValidationResponse; /// /// Throws a new one, crafting a decent exception message. /// /// The CAS ticket. /// The validation response. /// The inner exception. 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; } } } }