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