Introduction
Many people asked me if I couldn't show them an example of how to use Single Sign-On with the MSN Protocol 15. Of course, the hardest part for the most of them was, how to create the structure which we have to send back to the server and which needs a bit of knowledge about Cryptography.
But, of course, this article also addresses, to those who are interested, how MSN authentication works (at least with MSNP15), or want to have a general example of SSO.
Let's begin
First, of all, we will program the following classes:
MSNClient
TcpConnection
SOAPRequest
SSOticket
The primitive interface only consists of an input field for your MSN account, an input field for your password, and a log-in button.
If the user presses the button, we create a new MSN client with our account and our password:
private void button1_Click(object sender, EventArgs e)
{
Client = new MSNClient(textBox1.Text, textBox2.Text);
...
backgroundWorker1.RunWorkerAsync();
}
As you can see, we start a thread on which we will connect to MSN.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Client.Connect();
}
Now, let's have look at the Connect()
function in the class MSNClient
:
public void Connect()
{
string host = GetNSHostFromDispatchServer();
LogIntoNS(host.Split(':')[0], Convert.ToInt32(host.Split(':')[1]));
}
First, we connect to a Dispatch Server where we have to get the IP and the port for the notification server where we have to log in. For those who haven't got an idea of at least MSN protocol 8 and want to understand what I'm writing here, please read the basic stuff first ;-) There are enough sites out there which will help you.
At the others: as usual, first, we send the VER
command, then CVR
, and finally USR
. With MSNP15, the USR
command has got a new parameter: SSO
.
recvbuf = SendReceiveMSNCommand("USR", "SSO", "I", Account);
When we successfully connect to the notification server, we will send again the VER
, CVR
, and USR
commands. After the last request, we will receive a GCF
response whose meaning I don't know and which you shouldn't care, too. And after that, the server will send a new response which contains an XML and the following USR
command. The USR
response is important for us:
int indexOfUSR = recvbuf.IndexOf("USR");
recvbuf = recvbuf.Remove(0, indexOfUSR);
string policy = recvbuf.Split(' ')[4];
string nonce = recvbuf.Split(' ')[5];
string ticket = GetTicket(policy, nonce);
As you can see, the response contains a ticket and another string which can be something like "MBI_OLD" and which we call nonce. With this important information from the server, we will get the ticket. This was basic stuff until now. But now, it's getting interesting (at least for those who wrote me emails). Now, we have to get a ticket and a magic key from MSN to tinker us some nice new key. To do this, we have to create an XML which contains our username, password, and ticket. It's too long to display it here, so just have a look at my source code to see how it's built-on. Then, we have to make a SOAP request with this XML:
SOAPRequest SOAPRequest = new SOAPRequest(xml, "https://login.live.com/RST.srf");
And in the class SOAPRequest
:
public SOAPRequest(string XmlCode, string Host)
{
ServerRequest = (HttpWebRequest)WebRequest.Create(Host);
ServerRequest.Method = "POST";
Stream stream = ServerRequest.GetRequestStream();
stream.Write(Encoding.UTF8.GetBytes(XmlCode), 0,
Encoding.UTF8.GetBytes(XmlCode).Length);
stream.Close();
}
SOAP uses HTTP to exchange XML. So, we have to do an HTTP request to https://login.live.com/RST.srf and send our XML to the server by writing it to the network stream. Don't forget to set the method to POST ;-) After we have sent the XML, we have to get the response of the server:
public XmlDocument GetResponse()
{
HttpWebResponse ServerResponse =
(HttpWebResponse)ServerRequest.GetResponse();
Stream stream = ServerResponse.GetResponseStream();
XmlDocument xml = new XmlDocument();
xml.Load(stream);
stream.Close();
ServerResponse.Close();
return xml;
}
The response should contain a binary secret and a security token, but attention! There are two binary secrets in this weird chaos of XML nodes ;-). If your policy reference URI contained something like MBI
, MBI_SSL
, or MBI_KEY_OLD
, choose the one in <wsse:binarysecuritytoken id="Compactn">
where n is the same number as the RSTn request, else something like </wsse:binarysecuritytoken><wsse:BinarySecurityToken Id="PPTokenn"><wsse:binarysecuritytoken id="Compactn"><wsse:binarysecuritytoken id="PPTokenn"><wsse:binarysecuritytoken id="PPTokenn">
. Now, when we finally get our binary secret and the security token, the hard part can begin on break down :-o. No, seriously, it isn't really hard. You even don't have to know what these algorithms are doing. You just have to know that you have to send a structure to Windows and that you have to calculate some elements of it. As I said, at last, we have to send the following structure to the server (I took this example from here):
struct key
{
unsigned long uStructHeaderSize;
unsigned long uCryptMode;
unsigned long uCipherType;
unsigned long uHashType;
unsigned long uIVLen;
unsigned long uHashLen;
unsigned long uCipherLen;
unsigned char aIVBytes[8];
unsigned char aHashBytes[20];
unsigned char aCipherBytes[72];
}
At last, we have to send a string to the server, so we better make a structure in the form of an array. The only things we don't know in this structure are the last three ones. We can already program the beginning of the structure:
Beginning = new byte[28];
Beginning[0] = 0x1c;
Beginning[1] = 0x00;
Beginning[2] = 0x00;
Beginning[3] = 0x00;
Beginning[4] = 0x01;
Beginning[5] = 0x00;
Beginning[6] = 0x00;
Beginning[7] = 0x00;
Beginning[8] = 0x03;
Beginning[9] = 0x66;
Beginning[10] = 0x00;
Beginning[11] = 0x00;
Beginning[12] = 0x04;
Beginning[13] = 0x80;
Beginning[14] = 0x00;
Beginning[15] = 0x00;
Beginning[16] = 0x08;
Beginning[17] = 0x00;
Beginning[18] = 0x00;
Beginning[19] = 0x00;
Beginning[20] = 0x14;
Beginning[21] = 0x00;
Beginning[22] = 0x00;
Beginning[23] = 0x00;
Beginning[24] = 0x48;
Beginning[25] = 0x00;
Beginning[26] = 0x00;
Beginning[27] = 0x00;
Now, we want to create the "hash element" of the structure. We will create it with HMACSHA1. But first, we have to define a key:
byte[] key1 = Convert.FromBase64String(key);
string key2 = DeriveKey(key1, "WS-SecureConversationSESSION KEY HASH");
string key3 = DeriveKey(key1, "WS-SecureConversationSESSION KEY ENCRYPTION");
And in the function, DeriveKey()
:
private string DeriveKey(byte[] key, string magic)
{
HMACSHA1 sha = new HMACSHA1();
sha.Key = key;
byte[] Magic = Encoding.Default.GetBytes(magic);
byte[] hash1 = sha.ComputeHash(Magic);
byte[] hash2 = sha.ComputeHash(Combine(hash1, Magic));
byte[] hash3 = sha.ComputeHash(hash1);
byte[] hash4 = sha.ComputeHash(Combine(hash3, Magic));
byte[] o = { hash4[0], hash4[1], hash4[2], hash4[3] };
return Encoding.Default.GetString(Combine(hash2, o));
}
This is the way the keys are calculated. For our hash, we use key2
. There isn't more you have to know ;-) So, now, let's create the hash with this key:
HMACSHA1 sha = new HMACSHA1();
sha.Key = Encoding.Default.GetBytes(key2);
byte[] hash = sha.ComputeHash(Encoding.Default.GetBytes(nonce));
The aIVBytes
which also has to be added to our structure is nothing else than an array of random data. I was too lazy, so my array doesn't really calculate random numbers, but this isn't important for this example here. The last element which we need is aCipherBytes
, which we calculate by transforming the nonce with Triple DES and key3
as key:
TripleDESCryptoServiceProvider DES3 = new TripleDESCryptoServiceProvider();
DES3.Key = Encoding.Default.GetBytes(key3);
DES3.Mode = CipherMode.CBC;
DES3.IV = iv;
ICryptoTransform Encryptor = DES3.CreateEncryptor();
byte[] RestOfNonce = { 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 };
byte[] output = new byte[72];
Encryptor.TransformBlock(Combine(Encoding.Default.GetBytes(nonce),
RestOfNonce), 0, Combine(Encoding.Default.GetBytes(nonce),
RestOfNonce).Length, output, 0);
Now, we can merge everything to a structure:
string struc = Encoding.Default.GetString(Beginning) +
Encoding.Default.GetString(iv) + Encoding.Default.GetString(hash) +
Encoding.Default.GetString(output);
value = Convert.ToBase64String(Encoding.Default.GetBytes(struc));