kick it on DotNetKicks.com   Shout it  

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


Feedback

# 

Gravatar Wow - waiting the 4th part has been killing me! Hallelujah!

Nicely done! I will look to your posts about SSO for any future needs I have for this sort of thing, and refer others here as well. 7/9/2009 8:38 PM | noreply@blogger.com (JasonBunting)

# 

Gravatar Thanks, Jason. Also, I read the novel you left on my other post, but I'll have to wait to respond until I have an hour or two to address the points you brought up ;). 7/10/2009 6:19 AM | noreply@blogger.com (Mark J. Miller)

# 

Gravatar Could this service be used with a non-.NET application as well? 7/17/2009 12:46 PM | noreply@blogger.com (Anonymous)

# 

Gravatar Oops, saw your note about "In theory...", but I'm wondering how the service could use things like HttpContext.Current.User.Identity when you're not using .NET in the calling application. 7/17/2009 12:54 PM | noreply@blogger.com (Anonymous)

# 

Gravatar Hi, got it all to work calling from your client java script.

However, when I do test making a call from c# to signin I get an parse problem in the JSONPEncode factory. in the WriteMessage method.

I was on my way to implement a MembershipProvider ontop of this.
It throws and exception on Login(...)

public override bool ValidateUser(string username, string password)
{
SSOServiceClient client = null;
try
{

client = new SSOServiceClient("userEndpoint");
SSOToken token = client.Login(username,password);
client.Close();
if (token.Status == "SUCCESS")
return true;
}
catch (Exception ex)
{
if (client != null)
{
client.Close();
}

}
finally
{
if (client != null)
{
client.Close();
}
}
return false;
} 7/17/2009 2:26 PM | noreply@blogger.com (Idaho House - Herb)

# 

Gravatar Anonymous,

the service implements 2 separate interfaces. ISSOService is designed to only be used from a client such as a web browser. But the ISSOPartnerService could be used from any platform. I should have been more specific, but when I said any platform I meant ISSOPartnerService. But the bindings used to communicate via JSONP would most likely prevent you from communicating via any other client other than a web browser.

Herb,

My comments to Anonymous most likely apply in your case as well. It sounds like you're trying to wrap calls to Login/Logout in a membership provider. But the bindings on the WCF service are setup to require communication via JSONP. Add to that the dependency on passing a cookie back and forth and you're trying to fit a square peg in a round hole.

The idea behind the SSO service is that your clients don't sign in on your site, they sign in on the SSO site (via JSONP). Then once they signin they don't have to repeat the login for any other sites. In order to accomplish this the SSO service needs to place it's own cookie on the client so when an application other than yours (or a second application you've written) wants to authenticate the user they can do so between the user and the sso service. But if your application hides the sso service from the user by wrapping it with a membership provider and handling the login itself then the user will not be able to authenticate without signing in again.

You can still create a membership provider, but your membership provider will not use Login with a username password, the Login method will just accept a string - the authenticationTicket passed to it by the client. In an SSO senario the user should never send their username/password directly to your application, that's handled by the SSO service. 7/18/2009 6:27 AM | noreply@blogger.com (Mark J. Miller)

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

Gravatar Is it possible to expose one of these methods like RequestToken via JSONP *and* something like TCP/IP without having to write separate versions of the method? 8/14/2009 9:29 AM | Ron

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

Gravatar Ron,

Yes, it's possible. You'll need to configure an additional binding+endpoint your client service contract. I haven't had to do that specifically but it is possible. But unless you're configuring this to work with a windows client or silverlight client leaving it as is should work fine. Especially, because RequestToken uses the client's cookie (ie. the user logging in) - if you try and call RequestToken from the partner site it's possible, but you're not submitting the token from the user's client, you're submitting it from the "partner" application. 8/18/2009 2:20 PM | MarkJMiller

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

Gravatar Is it possible for a PDF/docx version of all these four parts to be combined and given as one document for offline reading? Its painful to copy all one by one/align etc :(..Thanks! 10/7/2009 1:01 AM | Ghanshyam

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

Gravatar Ghanshayam, here's a link to a Word version - files.getdropbox.com/u/273037/SSO%20Provider.docx 10/7/2009 9:11 AM | Mark J. MIller

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

Gravatar I couldn't download the source code from that location and it’s seemed to be service unavailable. Is it possible someone already have to source code please email to me? 12/21/2009 7:42 AM | Raj

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

Gravatar Please email the source code to my email mrajv76@yahoo.com 12/21/2009 7:44 AM | Raj

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

Gravatar Ron, Mark,

Microsoft's JSONP binding does not work out of the box when using other bindings/endpoints. e.g. a basicHttpBinding (SOAP) fails because the BeforeCall method in JSONPBehavior.cs fails to check if the WebOperationContext.Current.IncomingRequest.UriTemplateMatch != NULL (a SOAP request does not contain QueryParameters) Checking if the UriTemplateMatch is null fixes this problem.

Perhaps this helps someone!

W

1/8/2010 2:22 AM | Wouter

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

Gravatar Wouter, thanks!

Raj, try this link for the source code: www.developmentalmadness.com/samplecode/sso.zip 1/13/2010 4:31 PM | MarkJMiller

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

Gravatar Hi,

Just a question regarding security here... But, in theory, if someone was already logged in, and I momentarily had access to their browser, I could manually enter the URL for the RequestToken method, which would return the JSON containing the token in the browser, I could then copy the token, then on another machine (within a month) post the token to the validate controller in the application and hey presto, I have access...
Or did I miss something?

Cheers,
Ian 2/3/2010 7:11 AM | Ian

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

Gravatar Ian, Not sure where the 1 month number came from (unless I had that set as the expiration length- it's been a while and I'll have to check). But yes, that would be possible. But this doesn't expose anything that isn't already exposed when using FormsAuthentication. If you're using FormsAuthentication and you had access to the user's machine you could open the cookie file and get the same token anyway. This is why you should log out/delete cookies after you're done using a public machine. So how does this create an additional security concern? 2/3/2010 12:51 PM | MarkJMiller

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

Gravatar Any chance you can extend this to be implemented using ASP.NET MVC ? Or using Azure ? 5/8/2010 12:37 PM | Tom

Post a comment





 

Please add 6 and 5 and type the answer here:

 

 

Copyright © Mark J. Miller