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 () provider using the platform. If you just want to know about and and aren’t interested in implementing SSO read ahead. Otherwise, make sure to check out and . ( 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

Support for JSONP is built into the jQuery

Server-side JSONP Support

Even though client-side support for JSONP is built in

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

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

 
 
 
 

 type="Microsoft.Ajax.Samples.JsonpBindingExtension, SSO.Service, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
 
 
 
 
 
 
 
 
 
 
 


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

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()

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

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.