Using both OpenID and CAS authentication within the same MVC site

My employer uses CAS as the authentication tool for internal users, but does not allow accounts for external users. However sometimes we need to let external users on! So instead I added an additional authentication mechanism to login with an OpenID too. This article just shows the quick and (some might say) unrefined way of getting it working in ASP.NET MVC2 with SharpArchitecture 1.6. Some of this is based on this great article about getting OpenID to work with MVC3.

To start with download the DotNetOpenAuth .Net library, put it in your solution’s main lib directory, and then add a reference to it in your Controllers project.

Then download the OpenId-Selector javascript library and add the script, css and images to your Web project.

Next copy the CASP class from JaSig’s site and put it in your ApplicationServices project with a class name of ‘CASService’. This class was written for Webforms rather than MVC, and as it stands is not really unit testable, but it gives a good start, I’ll leave it up to you if you want to refactor it, but I didn’t bother for this proof-of-concept. The only changes you need to make are to first remove the currentPage property and replace any references to it with HttpContext.Current. Replace the currentPage parameter in the constructor and Authenticate methods with ‘string ReturnURL’ instead. The main constructor should now look like this:

    public CASP(string baseCasUrl, string ReturnURL, 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(HttpContext.Current.Request.Url.AbsoluteUri.Split('?')[0]+ "?ReturnUrl=" + ReturnURL);
        }

The authentication mode in web.config should be set to Forms, e.g.

<authentication mode="Forms">
        <forms loginUrl="~/Security/LogOn"  name="AuthCookie" timeout="60"/>
</authentication>

The first login page is basically just a method of selecting which authentication provider you want to use (note I’ve not styled this at all, just in case you were wondering :-)). I’m assuming here that you have developed your own master page and simple controller method to display this.

The master page should include these lines:

link href="<%= Url.Content("~/Content/openid-shadow.css") %>"
     rel="stylesheet" type="text/css" />
    <link href="<%= Url.Content("~/Content/openid.css")%>" 
     rel="stylesheet" type="text/css" />
    <script src="<%= Url.Content("~/Scripts/openid-en.js")%>" 
     type="text/javascript"></script>
    <script src="<%=Url.Content("~/Scripts/openid-jquery.js")%>" 
     type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            openid.init('openid_identifier');
        });
    </script>

And this is the login page:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/OpenId.Master" Inherits="System.Web.Mvc.ViewPage<IdeaFactory.ApplicationServices.ViewModels.LogOnViewModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
  <form action=
"CASAuthenticate?ReturnUrl=<%= HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"]) %>"
 method="post" id="cas_form">

<div>
    
        <legend>Login using  CAS</legend>
        
        <div id="Div2">
            <img src='<%= Url.Content("~/Content/Images/cas.gif")%>' />
            <input type="submit" value="Log On" />
        </div>
       
        <div>
           
        </div>
   
</div>
</form>

<br /><br />

    <form action=
"Authenticate?ReturnUrl=<%= HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"]) %>"
 method="post" id="openid_form">
<input type="hidden" name="action" value="verify" />
<div>
    
        <legend>Or login using OpenID</legend>
        <div class="openid_choice">
            <p>
                Please click your account provider:</p>
            <div id="openid_btns">
            </div>
        </div>
        <div id="openid_input_area">
            <%= Html.TextBox("openid_identifier") %>
            <input type="submit" value="Log On" />
        </div>
        <noscript>
            <p>
                OpenID is service that allows you to log-on to many different websites 
                using a single indentity. Find out <a href="http://openid.net/what/">
                 more about OpenID</a>and <a href="http://openid.net/get/">
                 how to get an OpenID enabled account</a>.</p>
        </noscript>
        <div>
           
        </div>
   
</div>
</form>
<br />

</asp:Content>

The LogInViewModel is simple, just enough to hold the OpenID returned by the javascript library:

 public class LogOnViewModel
    {
        public string openid_identifier { get; set; }
    }

Below are the raltively simple controller methods for authentication. Note that these have to be available as both HttpPost and HttpGet.

// Include these namespaces in your controller
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;

       public ActionResult CASAuthenticate(LogOnViewModel model, string ReturnUrl)
        {
            
           

            // Simple call to authenticate CAS.
            string username = CASP.Authenticate("https://login.casserver.com/cas/", ReturnUrl, false, false);
           
            try
            {
                if (username != null)
                {
                    _formsService.SignIn(username, false);



                    return Redirect(ReturnUrl);
                }
            }
            catch (Exception ex)
            {
              
            }

            return View("LogOn");
        }

        // Largely copied from http://weblogs.asp.net/haithamkhedre/archive/2011/03/13/openid-authentication-with-asp-net-mvc3-dotnetopenauth-and-openid-selector.aspx
        public ActionResult Authenticate(LogOnViewModel model, string returnUrl)
        {
            var response = openid.GetResponse();
            if (response == null)
            {
                //Let us submit the request to OpenID provider
                Identifier id;
                if (Identifier.TryParse(model.openid_identifier, out id))
                {
                    try
                    {
                        var request = openid.CreateRequest(
                                             model.openid_identifier);
                        return request.RedirectingResponse.AsActionResult();
                    }
                    catch (ProtocolException ex)
                    {
                     
                        return View("LogOn");
                    }
                }

              
                return View("LogOn");
            }

            //Let us check the response
            switch (response.Status)
            {

                case AuthenticationStatus.Authenticated:
                    LogOnViewModel lm = new LogOnViewModel();
                    lm.openid_identifier = response.ClaimedIdentifier;
                   
                    _formsService.SignIn(lm.openid_identifier, false);


                    return Redirect(returnUrl);

                case AuthenticationStatus.Canceled:
                    
                    return View("LogOn");
                case AuthenticationStatus.Failed:
                  
                    return View("LogOn");
            }

            return new EmptyResult();
        }

The _formsService class is what actually creates the login cookie:

public class FormsAuthenticationService:IFormsAuthenticationService
    {
        public void SignIn(string userName, bool createPersistentCookie)
        {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
            
            FormsAuthenticationTicket tkt;
            string cookiestr;
            HttpCookie ck;
            tkt = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddMinutes(30), false, "");
            cookiestr = FormsAuthentication.Encrypt(tkt);
            ck = new HttpCookie(FormsAuthentication.FormsCookieName, cookiestr);
            ck.HttpOnly = true;
            ck.Path = FormsAuthentication.FormsCookiePath;
            HttpContext.Current.Response.Cookies.Add(ck);
            
        }


        public void SignOut()
        {
            FormsAuthentication.SignOut();
        }
    }

And then if you want to set the Identity with the app then you can just go to Global.asax and add/edit the OnAuthenticationRequest method:

      protected void Application_OnAuthenticateRequest(object sernder, EventArgs e)
        {
          
                    // Extract the forms authentication cookie
                    string cookieName = FormsAuthentication.FormsCookieName;
                    HttpCookie authCookie = Context.Request.Cookies[cookieName];

                    if(null == authCookie)
                    {
                      // There is no authentication cookie.
                      return;
                    } 


                    FormsAuthenticationTicket authTicket = null;
                    try
                    {
                      authTicket = FormsAuthentication.Decrypt(authCookie.Value);
                    }
                    catch(Exception ex)
                    {
                      // Log exception details (omitted for simplicity)
                      return;
                    }

                    if (null == authTicket)
                    {
                      // Cookie failed to decrypt.
                      return; 
                    } 


                    // When the ticket was created, the UserData property was assigned a
                    // pipe delimited string of role names.
                    string[] roles = authTicket.UserData.Split(new char[]{'|'});


                    // Create an Identity object
                    FormsIdentity id = new FormsIdentity( authTicket ); 

                    // This principal will flow throughout the request.
                    GenericPrincipal principal = new GenericPrincipal(id, roles);
                    // Attach the new principal object to the current HttpContext object
                    Context.User = principal;


              
        }

Remember that when a user logs out of your site, they are not necessarily logged out of your identification provider, so you may want to provide a message that informs them of this.

So nothing particularly original, but as I couldn’t find anything specifically on this topic, and some of the information comes from some pretty hard-to-find places, I thought I’d post here to save others the time in searching.