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 using the ASP.NET platform. Make sure to check out , 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
  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
  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

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 binding="webHttpBinding" 
 bindingConfiguration="partnerBinding"
 contract="References.ISSOPartnerService" 
 name="partnerEndpoint" />
 


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 . In theory, if you know how to setup to communicate with other platforms other than the .NET Framework

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