DevelopMENTAL Madness

Thursday, July 30, 2009

Migrating away from Blogger.com

I’ve been underground since the beginning of the month and haven’t been working on any new posts. I have several new topics in the pipeline but I’ve been focusing on two projects that are keeping me away from my blog at the moment.

The first is digging into Silverlight. My employer has been getting requests for developers with Silverlight experience and so I’ve been working on digging in a bit more so I’m ready when I need it. I’m using my work on my project YouthSportsFan.com to give me something real-world to cut my teeth on and I’ll have some interesting posts that will come out of it. One is something I’ve already touched on lightly – design-time databinding. Also, I’ve been playing with custom templates and using them to improve the UI for a couple of the Silverlight Toolkit controls. I’m excited about both those upcoming posts.

The second thing I’ve been working on is indirectly related to my work on Silverlight. A colleague of mine has been very vocal (including bugging me in comments here on my blog) about getting me to switch away from Blogger.com. This got me thinking about how nice it would be to host running Silverlight samples in my blog posts instead of just the code snippets I usually post. So I’ve purchased my own domain developmentalmadness.com and it’s currently hosted on GoDaddy. I’ve downloaded Subtext as a blog engine and I’m customizing it at the moment.

My most important goal is that the links I’ve worked hard to get posted around the web will continue to work. I’ve put a lot of work into promoting my blog this year and I don’t want any of the links placed by myself or my readers to break. So far, Subtext has been brilliant in allowing me to tweak a few web.config settings which will allow me to respond to the requests I expect to be forwarded by Blogger.com when I switch to a custom domain. I’m excited about this and I will post a follow up when I’ve completed this so others can make the switch as well.

Wednesday, July 08, 2009

Google Chrome OS: Speculations Abound, What Do You Think?

I'm very curious to watch how the new Chrome OS will develop. I've been reading some of the buzz around it today and have even been reading some of the comments around it. I use Chrome (the web browser) everyday, it’s my main browser. As of this week I even got my wife to finally start using it after IE locked up her computer again and she decided it was the last time.

However, I doubt I will be ever using Chrome OS – or will I?

My prediction is that I am already using Chrome OS. If you follow the Chrome Dev Channel blog you’ve probably noticed that as the Google team is hard at work on v3 they are working on versions for Linux and Mac. They have been working on this since v2 was released back in May.

According to the Google blog Chrome OS will run on Linux and here are their stated goals:

  • run on both x86 as well as ARM chips
  • most of the user experience takes place on the web
  • Google Chrome running within a new windowing system on top of a Linux kernel
  • users don't have to deal with viruses, malware and security updates
  • data to be accessible to them wherever they are and not have to worry about losing their computer or forgetting to back up files
  • [users] don't want to spend hours configuring their computers to work with every new piece of hardware, or have to worry about constant software updates

These seemingly lofty goals are nothing new. For at least a year I’ve been hearing about software that allows you to bypass the OS and quickly boot straight to a “browser only mode”. Two examples are gOS and Presto. gOS is what I think Chrome OS will be like – the browser IS the OS. You boot directly into a web browser and your entire experience is inside the browser.

Applications

When Chrome was first released Google talked about how the browser was becoming (is?) its own platform for application development. They developed the Chrome JavaScript engine – V8 – to run JavaScript really fast to allow more to be done on the client. The idea is that if the JavaScript engine is “fast enough”, then it is adequate for most applications.

File Storage

Google Gears has been around for a while now and it means you don’t need local storage, other than a local cache. Gears allows you to make cross-domain requests using a “cross-origin worker”. This would allow your files to be stored in the cloud by any service provider (Flickr, Picasa, Windows Live) and then you can use any web application (doesn’t have to be the one provided by the aforementioned providers) to browse your photos as long as you grant it permission.

Video

The HTML 5 specification now makes video a first-class citizen of your web browser. Chrome supports playing video natively in the browser without needing a plug-in (pretty nice for Google since Chrome doesn’t support plug-ins). Again, you store your videos in the cloud and watch them anywhere.

Security

At least for now Chrome is the most secure web browser available. The security (simplified) is two tiered. The second tier being the sandbox. There are current exploits for Chrome, but those exploits land the attacker in the sandbox. This means that a hacker’s exploit must additionally exploit the sandbox. So a bug must expose both a problem with the browser’s rendering engine (Webkit) and expose a bug in the sandbox. So far, this has not been done. Could it be done? In theory, but until that happens there are no exploits for Chrome. Because of this, if Chrome were your entire OS then (speaking of today) you’re most likely you’re secure.

Applications

As a programmer, would I ever use Chrome OS? Right now, no. Developers require an IDE and debugging tools, source control integration and other such tools. All this requires software running locally on my desktop, but will it always be that way? Maybe not, just consider the Mozilla Labs Bespin project. At the moment it’s a far cry from Microsoft’s Visual Studio IDE, but do most developers really need all the tools provided by Visual Studio? I know I probably don’t use 10%. Statement completion, syntax highlighting and a compiler would be adequate. I’m not saying that’s all I use, other features make me much more productive but I’m saying it’s possible to provide many of the features in the cloud – even for an application developer.

For a graphics designer, you might think the browser might not be adequate either. But there are already services for the novice which allow you to edit photos, and with the addition of VML who’s to say that we’re so far from professional graphics programs hosted in the cloud either?

But for the average computer user, as time progresses people will become less objective of their files being located exclusively on the web. If Google can convince people that Google Docs has all the functionality they need along with other programs people use (photo/video editing) then it’s very conceivable that Chrome OS (and other similar OS’s) could be very successful.

Hardware Integration

So the only piece of this puzzle that isn’t immediately apparent to me at this point is how they will integrate with hardware devices. Maybe because I’m a web programmer and not an OS programmer. Google will need to allow you to get images off your camera, videos off your camcorder. Or at least read from a card reader. Then you need to upload those files to your preferred cloud provider. You’ll need to install devices like web cams and printers.

All this requires drivers and a way to access these devices. How do they plan on doing this without exposing vulnerabilities? Google has stated that if you want to develop applications for Chrome OS your platform is the web – HTML, JavaScript and any server side platform you choose. But the web is not currently allowed to access your devices (for good reason).

Conclusion

Chrome the web browser already does most of what Google has envisioned for their new OS. They just need to integrate it into a Linux distro (something they’ve already accomplished with Android) and provide an interface for managing/accessing hardware devices. I know that “just” is still a long stretch, but not that far away when you consider everything else they’ve already got done.

For now, I already have an operating system which loads in a matter of seconds - about 25, not much longer than it takes to fire up a web browser and load a web page. Windows 7 running on a SSD is pretty darn fast, I’ve written before about it. I've been running it for almost 2 months now and I still have free space on my drive and it’s showing no signs of slowing down. So I’ve got no need for an OS that only will load 10-15 seconds faster, but who’s to say.

Almost, 10 years ago, I said “hell no” when Ray Ozzy was talking about storing all our files on the web. But now I use Dropbox and Sky Drive. I’ve also used Google Docs a bit as well – but I don’t really create many docs or spreadsheets and for blogging Live Writer is better at keeping my formatting. So you could say I’ve bought into that philosophy over the years. And gradually I’m sure many others will too.

What do you think?

I really would like to know what you think. Am I dead wrong? Or dead on? How do you think they’ll integrate hardware? Do you think we’ll ever completely ditch the bloated operating systems of today?

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: , , ,

Monday, July 06, 2009

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

Using JSONP with WCF

This is the third article in a four part series on building a single sign on (SSO) provider using the ASP.NET platform. If you just want to know about JSONP and WCF and aren’t interested in implementing SSO read ahead. Otherwise, make sure to check out part 1 and part 2. (Part 4 and the source code are now available).

JSONP

Also known as “JSON with Padding” is more of a back door to allow cross domain AJAX requests. By dynamically generating <script /> tags in the current page it gets around the “Access to restricted URI denied” error you get when you try and access a resource which is not located within your domain.

Support for JSONP is built into the jQuery library, making it very easy to communicate between our client and SSO service. However, because the HTML <script /> tag is an http GET request, you cannot perform POST operations via JSONP.

Server-side JSONP Support

Even though client-side support for JSONP is built in if you’re using jQuery, server-side support is not. Unfortunately, you have to make special considerations for JSONP if you’re going to write services to support it. JSONP expects that your JSON data will be wrapped with a client-side callback like this:

callbackname({"property":"data"});

Where “callbackname” is the value of a query string parameter named “callback”. jQuery generates this callback at runtime, and expects it in the response so just check the value of Request.QueryString[“callback”] and replace “callbackname” with the value you get.

Do you see the problem yet? WCF controls the output stream here, so how do you handle this? According to Jason Kelly, you can download JSONP samples from MSDN, extract them and copy the following project files to your WCF project:

  • JSONPBehavior.cs
  • JSONPBindingElement.cs
  • JSONPBindingExtension.cs
  • JSONPEncoderFactory.cs

Then modify the service definition in your web.config by adding the extensions from the above files and modifing your bindings:

<system.serviceModel>
    <!-- add JSONP extensions -->
    <extensions>
        <bindingElementExtensions>
            <add name="jsonpMessageEncoding"
         type="Microsoft.Ajax.Samples.JsonpBindingExtension, SSO.Service, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        </bindingElementExtensions>
    </extensions>
    
    <!-- using JSONP bindings -->
    <bindings>
        <customBinding>
            <binding name="userHttp">
                <jsonpMessageEncoding />
                <httpTransport manualAddressing="true"/>
            </binding>
        </customBinding>
    </bindings>
        
</system.serviceModel>

Now you can tell WCF which operations need to support JSONP by applying the JSONPBehavior attribute like in the ServiceContract interface definition below:

[ServiceContract]
public interface ISSOService
{
    [OperationContract]
    [WebGet(UriTemplate="/RequestToken",
        BodyStyle=WebMessageBodyStyle.WrappedRequest,
        ResponseFormat=WebMessageFormat.Json)]
    [JSONPBehavior(callback = "callback")]
    SSOToken RequestToken();
 
    [OperationContract]
    // javascript can't post w/ jsonp - has to be WebGet
    [WebGet(UriTemplate="/Login?username={username}&password={password}",
        BodyStyle = WebMessageBodyStyle.WrappedResponse,
        ResponseFormat = WebMessageFormat.Json)]
    [JSONPBehavior(callback = "callback")]
    SSOToken Login(string username, string password);
 
    [OperationContract]
    [WebGet(UriTemplate = "/Logout",
        BodyStyle = WebMessageBodyStyle.WrappedResponse,
        ResponseFormat = WebMessageFormat.Json)]
    [JSONPBehavior(callback = "callback")]
    bool Logout();
}
 

These custom bindings don’t seem to play well when you want to expose your ServiceContract to other clients. This was the main reason why I split my ServiceContract into 2 separate interfaces. By using separate contracts I could define an endpoint for the client and one for the application, each using separate bindings. The client endpoint uses the custom JSONP bindings and the application (partner) endpoint uses webHttpBinding.

Service Implementation

After all this the service implementation is pretty vanilla although if you’re not following along the entire series, make sure that you either configure WCF to enable AspNetCompatibility or remove the attribute from the class definition below or you’ll get an error.

The Login operation (if the credentials are valid) generates and encrypts a FormsAuthenticationTicket and adds it to the Response.Cookies collection. The RequestToken operation reads the FormsAuthenticationTicket from the current Identity and returns it to the client and the ValidateToken operation decrypts the token and reads the UserData property, if that fails then the token isn’t valid. Here it is:

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

Client Communication

Now that we have a working service implementation, we need to access the service operations from our client. We’ll use jQuery to make this quick and simple:

$(function() {
    $("#logon").click(function() {
        $.get('http://localhost:21259/SSOService.svc/user/Login?callback=?',
            { username: $("#username").val(), password: $("#password").val() },
            function(ssodata) {
            if (ssodata.LoginResult.Status == 'DENIED') {
                // display some sort of 'login failed' message to the user
            } else {
                // the client now needs to get the authentication ticket from
                // the service and present it to the web application for 
                // verification
            }
        }, 'jsonp');
    });
});

The nice thing about jQuery here is that JSONP support is built-in. You can just use the $.get() method and specify the dataType as ‘jsonp’.

Notice the "?callback=?” query string added to the service url. “callback=?” tells jQuery to generate a callback method and forward that callback name to our service. The value of “callback” is immaterial since it will be handled behind the scenes between jQuery and WCF. Also, you can place “callback=?” anywhere in the query string. If you have other query string parameters you can either do as I have done and include them as part of the JSON object passed to the $.get() method or include them in the query string along with “callback=?”. So the URI could have looked like this:

'http://localhost:21259/SSOService.svc/user/Login?callback=?&username=foo&password=bar'

or like this:

'http://localhost:21259/SSOService.svc/user/Login?username=foo&password=bar&callback=?'

Security

If you’re following the entire series, I’m sure your tired of hearing this, but cross-domain browser communication is considered unsafe. Protect your application and your users from session hijacking by setting your cookies HttpOnly = true. Also, to prevent XSS attacks, make sure your service doesn’t allow unchecked user data to be included in your communication with the client.

HttpOnly = true prevents session hijacking by preventing any client-side scripts which may be in the web application (which is beyond the control of the service application) from reading the cookie.

jQuery takes the response from the service and passes it to EVAL(). Using whitelists to validate userdata sent in response to client requests will prevent XSS scripts from being executed by the client.

Conclusion

We’ve discussed SSO as well as configuring WCF to support FormsAuthentication and JSONP, next we’ll tie everything together and you’ll finally get your hands on the source code. See you for the next and final installment.

Labels: , ,