Wednesday, October 29, 2008

Windows Live Services is an infrastructure (I am trying to avoid words like 'ecosystem') that provides multiple services (contacts, photo albums, blogs, web storage, mail etc) available via federated authentication mechanism. While the majority of these services are exposed to the user as web pages, they are also available to regular applications. Microsoft own such as LIve Mail, Live Writer, Live Gallery as well as 3rd party via  Live Services SDK. I am going to provide a short introduction on accessing some of the Live Services programmatically from Compact .NET Framework applications.

The starting point of any Live Services interaction being authenticating the user, it is only natural that Microsoft has provided a .NET component that handles the authentication for applications. This component however is offered only for desktop applications. Nevertheless behind every Live Service interaction there is a web request of a sort, which suggests that it can be emulated using NETCF code. While there are several authentication options available in Windows Live, there is just one that is really suitable for smart clients - RPS (Relying Party Suites). Underneath it is simply an HTTP POST of some XML data over secure channel. Upon successful request the application receives an encoded authentication ticket valid typically for 24 hours (the exact expiration time is provided with the ticket). To request the ticket an application sends XML message to https://dev.login.live.com/wstlogin.srf. The message uses content type "application/soap+xml" and looks like this:

<s:Envelope
   
xmlns:s = "http://www.w3.org/2003/05/soap-envelope"
   
xmlns:wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
   
xmlns:saml = "urn:oasis:names:tc:SAML:1.0:assertion
   
xmlns:wsp = "http://schemas.xmlsoap.org/ws/2004/09/policy"
   
xmlns:wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
   
xmlns:wsa = "http://www.w3.org/2005/08/addressing"
   
xmlns:wssc = "http://schemas.xmlsoap.org/ws/2005/02/sc"
   
xmlns:wst = "http://schemas.xmlsoap.org/ws/2005/02/trust">

<s:Header>
   <
wlid:ClientInfo xmlns:wlid = "http://schemas.microsoft.com/wlid">
      <
wlid:ApplicationID>10</wlid:ApplicationID>
   </
wlid:ClientInfo>
   <
wsa:Action s:mustUnderstand = "1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
   <
wsa:To s:mustUnderstand = "1">https://dev.login.live.com/wstlogin.srf</wsa:To>
   <
wsse:Security>
      <
wsse:UsernameToken wsu:Id = "user">
         <
wsse:Username>example@live.com</wsse:Username>
         <
wsse:Password>S3cr3t</wsse:Password>
      </
wsse:UsernameToken>
   </
wsse:Security>
</
s:Header>

<s:Body>
   <
wst:RequestSecurityToken Id = "RST0">
      <
wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
      <
wsp:AppliesTo>
         <
wsa:EndpointReference>
            <
wsa:Address>http://live.com</wsa:Address>
         </
wsa:EndpointReference>
      </
wsp:AppliesTo>
      <
wsp:PolicyReference URI = "MBI"></wsp:PolicyReference>
   </
wst:RequestSecurityToken>
</
s:Body>

</s:Envelope>

Naturally, in the above XML the wsse:Username and wsse:Password will have to be substituted with actual user-provided credentials. In response we expect a standard SOAP message that has action set to http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue. In the body of this message of particular interest are the following parts:

<wst:Lifetime>
   <
wsu:Created>2008-10-29T14:38:23Z</wsu:Created>
   <
wsu:Expires>2008-10-29T22:38:23Z</wsu:Expires>
</
wst:Lifetime>

and

<wst:RequestedSecurityToken>
   <
wsse:BinarySecurityToken Id="Compact0">t=[very long Base64-encoded string here]</wsse:BinarySecurityToken>
</
wst:RequestedSecurityToken>

The former tells us the ticket validity time range. There is no need to reacquire a ticket until Expires date. The latter contains the actual ticket. Notice that while it has a format t=IKnvriu73nf..., the actual requests sent later to the Live servers will require the Base64 part to be quoted like this: t="IKnvriu73nf..."

Of course the application should be prepared to deal with authentication failures. A logon failure (bad password) will result in a message that looks like this:

  <S:Body>
    <S:Fault>
      <S:Code>
        <S:Value>S:Sender</S:Value>
        <S:Subcode>
          <S:Value>wst:FailedAuthentication</S:Value>
        </S:Subcode>
      </S:Code>
      <S:Reason>
        <S:Text xml:lang="en-US">Authentication Failure</S:Text>
      </S:Reason>
      <S:Detail>
        <psf:error>
          <psf:value>0x80048821</psf:value>
          <psf:internalerror>
            <psf:code>0x80041012</psf:code>
            <psf:text>The entered and stored passwords do not match.</psf:text>
          </psf:internalerror>
        </psf:error>
      </S:Detail>
    </S:Fault>
  </S:Body>
</S:Envelope>

Notice that it conveniently includes human-readable failure reason as well as a hex error code.

The code to request the ticket would look like this (sans error handling):

public static string AcquireTicket(string userName, string password)
{
   const string url = @"https://dev.login.live.com/wstlogin.srf";
   HttpWebRequest request = WebRequest.Create(url)as HttpWebRequest ;
   request.Method = "POST";
   request.ContentType = "application/soap+xml; charset=UTF-8";
   request.Timeout = 10 * 1000; // Wait for at most 10 seconds
   byte[] bytes = System.Text.Encoding.UTF8.GetBytes(string.Format( soapEnvelope, userName, password));
   request.ContentLength = bytes.Length;
   Stream reqStream = request.GetRequestStream();
   reqStream.Write(bytes, 0, bytes.Length);
   reqStream.Close();
   WebResponse response;
   response = request.GetResponse();
   string xml;
   using (System.IO.StreamReader reader = new System.IO.StreamReader(response.GetResponseStream()))
      xml = reader.ReadToEnd();
   response.Close();
   XmlDocument document = new XmlDocument();
   document.LoadXml(xml);
   XmlNamespaceManager nsManager = new XmlNamespaceManager(document.NameTable);
   nsManager.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
   XmlNode node = document.SelectSingleNode(@"//wsse:BinarySecurityToken/text()", nsManager);
   if (node == null)
      return null; // The wsse:BinarySecurityToken element is missing. Examine the xml for error information
   else
      return node.Value;
}

To be continued: 

10/29/2008 9:13:32 AM (Pacific Daylight Time, UTC-07:00)  #    Comments [0]  | 
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):