Live Messenger over XMPP Refresh Tokens
24 November, 2011While I was previously unable to get refresh tokens working in Live Messenger over XMPP, I've finally figured it out. To be fair, the documentation did have it tucked away under the Reference/OAuth 2 section, not under XMPP or Messenger sections but really, I should have just RTFM a little bit better.
There are two types of "authorisation flow" - implicit and code and the difference starts with the initial request. Only code flow can get refresh tokens, implicit flow is designed as short lived (one hour) access. Having response_type=token in the url starts implicit flow, but having response_type=code starts code flow.
Using Code Flow
Where we previously had
https://oauth.live.com/authorize?client_id=CLIENTID&redirect_uri=https://oauth.live.com/desktop&response_type=token&scope=SCOPES
It should now be
https://oauth.live.com/authorize?client_id=CLIENTID&redirect_uri=https://oauth.live.com/desktop&response_type=code&scope=SCOPES
If you want a refresh token, you must have wl.offline_access in your scopes. Ideally, scopes should look like
scope=wl.basic%20wl.offline_access%20wl.messenger
Implicit flow (as the name implies) restricts access and thus doesn't need to do as thorough checking of who you say you are. You still can't get in without user authorisation and the proper ClientID, but a great deal less processing is required. By contrast code flow has two steps of confirmation and require your client secret code as well.
In implicit flow, once you have directed the user to the login url and they've granted access, you're redirected instantly to your redirect_uri with the auth token attached in the uri fragment. With code flow, you're redirected to your redirect_uri but get back a code used to then confirm you are who you say you are - not the actual auth token. If you're using the default redirect_uri, that url will look something like https://oauth.live.com/desktop?code=<code>.
At this point you can close the browser view from the user, as they don't need to interact with it anymore. Your code, however, still needs to make one final web call to
https://oauth.live.com/token?client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&client_secret=CLIENT_SECRET&code=AUTHORIZATION_CODE&grant_type=authorization_code
You'll then get back a JSON payload that looks like
{
"access_token" : "a_really_long_string",
"authentication_token" :"another_really_long_string",
"expires_in" :3600,
"refresh_token" : "final_long_string",
"scope" : "wl.basic wl.messenger wl.offline_access",
"token_type" : "bearer"
}
Finally! We have our refresh token! Remember, you don't have to reconnect every hour, it's just that new connections after the token expires (DateTime.Now.Add(new TimeSpan(1, 0, 0))) will need the access token 'refreshed' first, and to do that, you have to send another web request to
https://oauth.live.com/token?client_id=[YOUR_CLIENT_ID]&client_secret=[YOUR_CLIENT_SECRET]&redirect_uri=REDIRECT_URL&grant_type=refresh_token&refresh_token=[REFRESH_TOKEN]
Again you'll get back a JSON payload that looks almost identical to the one above, just missing the authentication_token.
Coding it all
These changes make my previous OAuth2Helper somewhat less useful. It'll still work, just in less useful situations. Using parts of the code from Microsoft's LiveSDK on GitHub I cobbled together a new helper library that should handle it from start-to-finish. This should take on no external dependencies, but if you were to start using the rest of the LiveSDK API, I'd recommend switching to something like RestSharp and JSON.NET.
I've taken the effort to make sure all scopes are available in the enum, most of them don't need to be used unless you're using the REST API as well. Many of the scope options are supersets of previous scopes, for example ContactsBirthday gives access to your contacts birthday and the logged in users birthday, whereas Birthday just gives the latter. Information is up on MSDN about all the permissions each scope gives.
I also realise this isn't ideal - no error checking nor is it asynchronous - it is meant as demo code to prove a point, if somebody is actually interested in this, I'm happy to lend a hand to make it more usable.
[DataContract]
public class OAuthToken
{
[DataMember(Name = OAuthConstants.AccessToken)]
public string AccessToken { get; set; }
[DataMember(Name = OAuthConstants.RefreshToken)]
public string RefreshToken { get; set; }
[DataMember(Name = OAuthConstants.ExpiresIn)]
public string ExpiresIn{get; set;}
[DataMember(Name = OAuthConstants.Scope)]
public string Scope { get; set; }
}
public static class OAuthConstants
{
public const string ClientID = "client_id";
public const string ClientSecret = "client_secret";
public const string Callback = "redirect_uri";
public const string ClientState = "state";
public const string Scope = "scope";
public const string Code = "code";
public const string AccessToken = "access_token";
public const string ExpiresIn = "expires_in";
public const string RefreshToken = "refresh_token";
public const string ResponseType = "response_type";
public const string GrantType = "grant_type";
public const string Error = "error";
public const string ErrorDescription = "error_description";
public const string Display = "display";
}
[Flags]
public enum Scope
{
[Description("wl.basic")]
Basic = 1,
[Description("wl.messenger")]
Messenger = 2,
[Description("wl.offline_access")]
Offline = 4,
[Description("wl.signin")]
Signin = 8,
[Description("wl.birthday")]
Birthday = 16,
[Description("wl.calendars")]
Calendars = 32,
[Description("wl.calendars_update")]
CalendarsUpdate = 64,
[Description("wl.contacts_birthday")]
ContactsBirthday = 128,
[Description("wl.contacts_create")]
ContactsCreate = 256,
[Description("wl.contacts_calendars")]
ContactsCalendars = 512,
[Description("wl.contacs_photos")]
ContactsPhotos = 1024,
[Description("wl.contacts_skydrive")]
ContactsSkydrive = 2048,
[Description("wl.emails")]
Emails = 4096,
[Description("wl.events_create")]
EventsCreate = 8192,
[Description("wl.phone_numbers")]
PhoneNumbers = 16384,
[Description("wl.photos")]
Photos = 32768,
[Description("wl.postal_addresses")]
PostalAddresses = 65536,
[Description("wl.share")]
Share = 131072,
[Description("wl.skydrive")]
Skydrive = 262144,
[Description("wl.skydrive_update")]
SkydriveUpdate = 524288,
[Description("wl.work_profile")]
WorkProfile = 1048576,
[Description("wl.applications")]
Applications = 2097152,
[Description("wl.applications_create")]
ApplicationsCreate = 4194304
}
public static class OAuth2Helper
{
public static Uri GenerateRequestUrl(string clientId, Scope scopes, string type="token", string callback="https://oauth.live.com/desktop", string display = "touch")
{
string scopeString = "";
var y = Enum.GetValues(typeof(Scope)).Cast<Scope>();
foreach (var s in y)
{
if ((scopes & s) == s)
scopeString += GetDescription(s) + "%20";
}
return new Uri(String.Format(@"https://oauth.live.com/authorize?client_id={0}&display={2}&redirect_uri={4}&response_type={3}&scope={1}", clientId, scopeString, display, type, callback));
}
public static OAuthToken RefreshToken(string clientId, string clientSecret, string refreshToken, string callback = "https://oauth.live.com/desktop")
{
var wc = new WebClient();
var url = string.Format("https://oauth.live.com/token?client_id={0}&redirect_uri={1}&client_secret={2}&refresh_token={3}&grant_type=refresh_token", clientId, callback, clientSecret, refreshToken);
var response = wc.DownloadString(url);
var ms = new MemoryStream(Encoding.Unicode.GetBytes(response));
var serializer = new DataContractJsonSerializer(typeof(OAuthToken));
return serializer.ReadObject(ms) as OAuthToken;
}
public static string CodeFromUriResponse(Uri response)
{
if (!response.Query.Contains("code"))
return null;
var nvPair = Regex.Split(response.Query.Substring(1), "=");
return nvPair[0] == "code" ? nvPair[1] : null;
}
public static string AccessTokenFromUriResponse(Uri response)
{
if (!response.Fragment.Contains("access_token"))
return string.Empty;
var responseAll = Regex.Split(response.Fragment.Remove(0, 1), "&");
return (from t in responseAll
select Regex.Split(t, "=") into nvPair
where nvPair[0] == "access_token"
select nvPair[1]).FirstOrDefault();
}
public static OAuthToken AuthCodeToToken(string clientId, string clientSecret, string auth, string callback = "https://oauth.live.com/desktop")
{
var wc = new WebClient();
var url = string.Format("https://oauth.live.com/token?client_id={0}&redirect_uri={1}&client_secret={2}&code={3}&grant_type=authorization_code", clientId, callback, clientSecret, auth);
var response = wc.DownloadString(url);
var ms = new MemoryStream(Encoding.Unicode.GetBytes(response));
var serializer = new DataContractJsonSerializer(typeof(OAuthToken));
return serializer.ReadObject(ms) as OAuthToken;
}
public static string GetDescription<T>(T value) where T : struct
{
string name = value.ToString();
object[] attrs =
value.GetType().GetField(name).GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
return (attrs.Length > 0) ? ((System.ComponentModel.DescriptionAttribute)attrs[0]).Description : name;
}
public static T GetEnumFromDescription<T>(string value)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("value must be non-empty");
}
var field = typeof(T).GetFields().FirstOrDefault(f =>
{
var attrs = f.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
if (attrs.Length > 0)
{
if (((System.ComponentModel.DescriptionAttribute)attrs[0]).Description == value)
{
return true;
}
}
return false;
});
if (field != null)
{
return (T)field.GetValue(null);
}
else
{
return (T)Enum.Parse(typeof(T), value);
}
}
}
Usage
To start the OAuth2 process
webBrowser.Navigate(OAuth2Helper.GenerateRequestUrl("clientid", Scope.Messenger | Scope.Offline, "code"));
To get the token back
webBrowser.LoadCompleted += (s, e) =>
{
var code = OAuth2Helper.CodeFromUriResponse(e.Uri);
if (!string.IsNullOrEmpty(code))
{
var token = OAuth2Helper.AuthCodeToToken("clientid", "secret", code);
}
}
Where does that leave us?
While refresh tokens are annoying, using it correctly does solve one of the major issues I had with MSNXMPP. However, this doesn't solve the "idle" (XEP-0199) issue that caused the connection to be dropped, nor non-standard Roster management.

