ASP.NET Forms Authentication

ASP.NET has two authentication models. The default uses your existing Windows logon and IE to identify you to the Web Server without any interaction (although this also applies to Basic Authentication where the Browser pops up a dialog box). The other approach is called Forms Authentication. Microsoft expects an application using this technique to code a page that presents a Form with a textbox for Userid and Password. The form then validates the two fields and passes the authenticate userid back to the .Net Framework. In this code an ASP.NET page is created that claims to be the Forms Authentication page, but instead of asking the user directly for Userid and Password it redirects to CAS, validates the ticket, and then passes the Userid back to the .NET Framework.

Objectively, .Net doesn't care what method is used to authenticate the user. So this code is no better than other .NET authentication technologies, and in some cases you might prefer some sort of filter that can also make decisions. This code is provided as an example because it is the closest match in spirit and design between the CAS service and any specific ASP.NET authentication model. It also uses the best documented and most "application developer" oriented programming interface.

.NET runs in an environment called the CLR just as JSP runs in a JVM. It is configured by a Web.Config file in the directory just as Servlets are configured by web.xml. One configuration option in both Web.Config and web.xml is how the pages and applications in the directory are secured. One of the ASP.NET options is to use "Forms Authentication", where access to a set of pages requires authentication through a login page. Typically a Web.Config is generated automatically by Visual Studio. Change the existing authentication to:

<authentication mode="Forms" >
  <forms name="casauth" loginUrl="login.aspx" />
</authentication>
<authorization>
  <deny users="?" />
</authorization>

This tells ASP.NET to require authentication for all access to the directory and to internally reroute every unauthenticated user to the login.aspx page. You can read the Microsoft documentation if you want a more limited scope of protection.

Now in Visual Studio create a login.aspx page with CS as the language. I suggest dragging a Label object to it from the Toolbox, which will become Label1 and will be used for an error message if something goes wrong. There is nothing more on the "login web page". The rest is done with code.

In the generated login.aspx.cs file, add System.IO, System.Net, System.Web, and System.Xml to the list of using statements (imports for you Java folk). The rest is to code a Page_Load event handler. You can probably improve on:

// Local specific CAS host
private const string CASHOST = "https://secure.its.yale.edu/cas/";

// After the page has been loaded, this routine is called.
protected void Page_Load(object sender, EventArgs e)
{
  // Look for the "ticket=" after the "?" in the URL
  string tkt = Request.QueryString[CAS:"ticket"];

  // This page is the CAS service=, but discard any query string residue
  string service = Request.Url.GetLeftPart(UriPartial.Path);

  // First time through there is no ticket=, so redirect to CAS login
  if (tkt == null || tkt.Length == 0)
  {
    string redir = CASHOST + "login?" +
      "service=" + service;
    Response.Redirect(redir);
    return;
  }

  // Second time (back from CAS) there is a ticket= to validate
  string validateurl = CASHOST + "serviceValidate?" +
    "ticket=" + tkt + "&"+
    "service=" + service;
  StreamReader Reader = new StreamReader( new WebClient().OpenRead(validateurl));
  string resp = Reader.ReadToEnd();
  // I like to have the text in memory for debugging rather than parsing the stream

  // Some boilerplate to set up the parse.
  NameTable nt = new NameTable();
  XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt);
  XmlParserContext context = new XmlParserContext(null, nsmgr, null, XmlSpace.None);
  XmlTextReader reader = new XmlTextReader(resp, XmlNodeType.Element, context);

  string netid = null;

  // A very dumb use of XML. Just scan for the "user". If it isn't there, its an error.
  while (reader.Read())
  {
    if (reader.IsStartElement()) {
      string tag = reader.LocalName;
      if (tag=="user")
        netid = reader.ReadString();
    }
  }
  // if you want to parse the proxy chain, just add the logic above
  reader.Close();
  // If there was a problem, leave the message on the screen. Otherwise, return to original page.
  if (netid == null)
  {
    Label1.Text = "CAS returned to this application, but then refused to validate your identity.";
  }
  else
  {
    Label1.Text = "Welcome " + netid;
    FormsAuthentication.RedirectFromLoginPage(netid, false); // set netid in ASP.NET blocks
  }
}

Now in any page in the application, the netid is accessible as Context.User.Identity.Name.

For an alternative code sample that also implements a Role Provider see ASP.NET Forms Authentication with Role Provider.

Another alternative code sample based on this tutorial that supports Single Sign Out with sample app is available here.