DevelopMENTAL Madness

Tuesday, July 07, 2009

Building a Single Sign On Provider Using ASP.NET and WCF: Part 4

This is the fourth and final article in a four part series on building a single sign on (SSO) provider using the ASP.NET platform. Make sure to check out part 1, part 2 and part 3.

Source Code

Implementing a Single Signon Provider

This is all a rehash since I’ve covered each point in detail to this point, but I’d like to tie everything together at this point and provide the source code. If you’d like detailed descriptions about how/why review the previous 3 parts. The full source code will be available here.

SSOFlowDiagram

  1. When an unauthenticated client requests a secured resource from the application that client is redirected to an authentication page.
  2. The authentication page makes a request (via JSONP) to the SSO service for a token which can then be presented to the application as evidence of the client’s identity with the SSO service.
  3. If the client has already authenticated with the SSO service and has an active session then skip to step #7 otherwise the request is denied.
  4. An unauthenticated client (SSO authentication) is redirected to a login page where the client then submits credentials for the SSO service.
  5. Upon submitting a valid set of credentials to the SSO service the client receives a cookie containing a token which is valid for the SSO service.
  6. Now that the client has successfully authenticated with the SSO service the client is redirected back to the application’s authentication page (step #2).
  7. The client receives an encrypted copy of the authentication ticket from the SSO service which it can then submit to the application. NOTE: This extra step is required when cookies are set to “HttpOnly = true” because they cannot be accessed via client script (javascript).
  8. The client now submits the SSO token to the application. The application verifies the token with the SSO service by forwarding it and asking if it is a valid token.
  9. The SSO service responds to the application with a flag indicating wither or not the submitted token is valid or not. Potentially, the SSO service could also provide additional information regarding the identity of the client. If the token was valid, the application then responds to the client with a token of it’s own which identifies the client to the application.
  10. The client, now authenticated with both the SSO service as well as the application, resubmits the request for the resource from step #1.

Service Implementation

We’re using the FormsAuthentication API within WCF to manage identity

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class SSOService : ISSOService, ISSOPartnerService
{
    #region ISSOService Members
 
    public SSOToken RequestToken()
    {
        SSOToken token = new SSOToken
        {
            Token = string.Empty,
            Status = "DENIED"
        };
 
        if (HttpContext.Current.Request.IsAuthenticated)
        {
            FormsIdentity identity = (FormsIdentity)HttpContext.Current.User.Identity;
 
            token.Token = FormsAuthentication.Encrypt(identity.Ticket);
            token.Status = "SUCCESS";
        }
 
        return token;
    }
 
    public bool Logout()
    {
        HttpContext.Current.Session.Clear();
        FormsAuthentication.SignOut();
        HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName);
        cookie.Expires = DateTime.Now.AddDays(-10000.0);
        HttpContext.Current.Response.Cookies.Add(cookie);
        return true;
    }
 
    public SSOToken Login(string username, string password)
    {
        SSOToken token = new SSOToken
        {
            Token = string.Empty,
            Status = "DENIED"
        };
 
        // authenticate user
        if (string.CompareOrdinal("foo", username) == 0
            && string.CompareOrdinal("bar", password) == 0)
        {
            Guid temp = Guid.NewGuid();
 
            DateTime issueDate = DateTime.Now;
            DateTime expireDate = issueDate.AddMonths(1);
 
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, issueDate, expireDate, true, temp.ToString());
            string protectedTicket = FormsAuthentication.Encrypt(ticket);
 
            HttpCookie authorizationCookie = new HttpCookie(FormsAuthentication.FormsCookieName, protectedTicket);
            authorizationCookie.Expires = expireDate;
            authorizationCookie.HttpOnly = true;
 
            HttpContext.Current.Response.Cookies.Add(authorizationCookie);
 
            token.Status = "SUCCESS";
            token.Token = protectedTicket;
        }
 
        return token;
    }
 
    public SSOUser ValidateToken(string token)
    {
        try
        {
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(token);
 
            return new SSOUser { 
                Username = ticket.Name, 
                SessionToken = new Guid(ticket.UserData) 
            };
        }
        catch
        {
            return new SSOUser { 
                Username = string.Empty, 
                SessionToken = Guid.Empty 
            };
        }
    }
 
    #endregion
}

Web Application Client

Web.Config – system.serviceModel definition

<system.serviceModel>
    <bindings>
        <webHttpBinding>
            <binding name="partnerBinding" >
            </binding>
        </webHttpBinding>
    </bindings>
    <behaviors>
        <endpointBehaviors>
            <behavior name="partnerEndpointBehavior">
                <webHttp/>
            </behavior>
        </endpointBehaviors>
    </behaviors>
    <client>
        <endpoint address="http://localhost:21259/SSOService.svc/partner" behaviorConfiguration="partnerEndpointBehavior"
                            binding="webHttpBinding" 
                            bindingConfiguration="partnerBinding"
                            contract="References.ISSOPartnerService" 
                            name="partnerEndpoint" />
    </client>
</system.serviceModel>

For the web application all that is required is to call the ValidateToken method of the SSO service and then provide the client with a token that identifies the client for the ASP.NET application (Authenticate method calls FormsAuth.SignIn()):

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult Authenticate(string token, bool createPersistentCookie)
{
    SSOPartnerServiceClient client = new SSOPartnerServiceClient("partnerEndpoint");
    SSOUser user = client.ValidateToken(token);
 
    if (string.IsNullOrEmpty(user.Username)
        || Guid.Empty.Equals(user.SessionToken))
    {
        return Json(new { result = "DENIED" });
    }
 
    FormsAuth.SignIn(user, createPersistentCookie);
 
    return Json(new { result = "SUCCESS" });
}
 
public void SignIn(SSOUser user, bool createPersistentCookie)
{
    DateTime issueDate = DateTime.Now;
    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, user.Username,
        issueDate, issueDate.AddMinutes(20), true, user.SessionToken.ToString());
 
    string protectedTicket = FormsAuthentication.Encrypt(ticket);
 
    HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, protectedTicket);
    cookie.HttpOnly = true;
    cookie.Expires = issueDate.AddMinutes(20);
 
    HttpContext.Current.Response.Cookies.Add(cookie);
}

jQuery Client

Authenticate.aspx View

$(function() {
    // get valid token from SSO
    $.get('http://localhost:21259/SSOService.svc/user/RequestToken?callback=?', {},
        function(ssodata) {
            var logonPage = '<%=Url.Action("LogOn", "Account") %>';
 
            if (ssodata.Status == 'SUCCESS') {
                // get target url
                var redirect = '<%=Request["redirectUrl"] %>';
                if (redirect == '')
                    redirect = '<%=Url.Action("Index", "Home") %>';
 
                // validate SSO token thru current application
                $.post('<%=Url.Action("Authenticate", "Account") %>',
                    { token: ssodata.Token, createPersistentCookie: true },
                        function(data) {
                            if (data.result == 'SUCCESS')
                                document.location = redirect;
                            else
                                document.location = logonPage;
                        }, 'json');
            } else {
                // not logged into SSO service, go to login page
                document.location = logonPage;
            }
        // make sure to specify JSONP
        }, 'jsonp');
});

Logon.aspx View

$(function() {
    $("#logon").click(function() {
        $("#error").text('').hide();
        $.get('http://localhost:21259/SSOService.svc/user/Login?callback=?',
            { username: $("#username").val(), password: $("#password").val() },
            function(ssodata) {
            if (ssodata.LoginResult.Status == 'DENIED') {
                $("#error").text('Login Failed').show();
            } else {
                document.location = '<%=Url.Action("Authenticate", "Account") %>';
            }
        }, 'jsonp');
    });
});

Conclusion

At this point you have everything you need to implement an SSO provider using ASP.NET. In theory, if you know how to setup WCF to communicate with other platforms other than the .NET Framework (something that is beyond the scope of this article) your SSO service can be used across platforms as well as domains.

If the scope of the applications you are targeting is smaller (they’re all part of the same domain or even on the same machine) there are certainly simpler ways to accomplish the same result with less effort. This is an example of a provider which can cover a group of applications from any domain and across any platform/hardware boundaries.

I’ve really learned a lot in this exercise, thanks for following me through this. I hope you enjoyed it as well.

Source Code

Labels: , , ,

Links to this post:

Create a Link

<< Home